Skip to content

fix(console) Async frame deduplication - #26028

Closed
gavinhailey wants to merge 7 commits into
oven-sh:mainfrom
gavinhailey:deduplicate-async-stack-frames
Closed

gavinhailey wants to merge 7 commits into
oven-sh:mainfrom
gavinhailey:deduplicate-async-stack-frames

Conversation

@gavinhailey

@gavinhailey gavinhailey commented Jan 12, 2026 •

Copy link
Copy Markdown

What does this PR do?

This PR fixes duplicate consecutive stack frames in async functions (#22339) by implementing frame deduplication logic in Bun's stack trace generation. When JavaScriptCore generates stack traces for async functions, it creates duplicate frames on adjacent lines (one for the function declaration, another for the await expression). This PR filters these duplicates to match Node.js behavior, while preserving actual recursive async calls.

This behavior also seems to cause issues with Nx in #25695

The fix operates in two key locations:

  • ErrorStackTrace.cpp - When building JSCStackTrace from frames during error construction
  • FormatStackTraceForJS.cpp - When formatting stack traces into strings for Error.stack

The deduplication logic compares consecutive frames and removes duplicates when they:

  • Have the same function name
  • Come from the same source file
  • Are on adjacent lines (≤1 line apart)

How did you verify your code works?

  • Created comprehensive test suite in test/regression/issue/22339.test.ts covering:
    • Basic async function call stacks (verifies no consecutive duplicates)
    • Nested async functions (multiple levels of async calls)
    • Actual async recursion (ensures recursive frames are preserved)
    • Mixed sync/async code paths
    • Error.captureStackTrace with caller argument
    • Regular synchronous functions (ensures no regression)
  • Added additional stack formatting tests in test/js/bun/test/stack-format.test.ts covering:
    • Deep async call stacks (5+ levels)
    • Actual recursive async functions
    • Sync-only stacks
    • Mixed sync and async stack-format.test.ts
    • Constructed errors (not thrown)
    • Promise rejection stacks
    • Verified tests pass: Ran bun bd test test/regression/issue/22339.test.ts and bun bd test test/js/bun/test/stack-format.test.ts - all tests pass
    • Verified test failure before fix: Confirmed tests fail with USE_SYSTEM_BUN=1 bun test <file>, ensuring they actually validate the fix

@coderabbitai

coderabbitai Bot commented Jan 12, 2026 •

Copy link
Copy Markdown
Contributor

Walkthrough

This PR introduces duplicate-frame and internal-builtin-frame filtering for stack traces in Bun. It adds helper functions to detect consecutive duplicate async frames and internal builtin frames, refines frame collection logic during error stack trace assembly, and updates stack trace formatting to apply filtering to the output.

Changes

Cohort / File(s) Summary
Stack Trace Assembly & Filtering
src/bun.js/bindings/ErrorStackTrace.cpp, src/bun.js/bindings/ErrorStackTrace.h
Introduces duplicate-frame and internal-builtin-frame detection with new helpers: compareFramesForDuplicates(), isConsecutiveDuplicateAsyncFrame() (two overloads), and isInternalBuiltinFrame(). Refines fromExisting() and getFramesForCaller() to skip private frames, internal builtins, and consecutive async duplicates while tracking the last non-builtin frame. Exposes two public functions in Zig namespace.
Stack Trace Formatting
src/bun.js/bindings/FormatStackTraceForJS.cpp
Applies frame filtering by building filteredIndices and removes internal/private frames, internal builtins, and consecutive duplicate async frames. Updates frame counting, error syntax handling, and rendering loops to use the filtered frame set.
Test Coverage
test/js/bun/test/stack-format.test.ts, test/regression/issue/22339.test.ts
Introduces two new test suites validating stack formatting across async chains, recursion, sync/async mixtures, Promise rejections, and mixed call patterns. Tests verify deduplication, absence of asyncFunctionResume, and correct frame presence across various scenarios.

Possibly related PRs

Suggested reviewers

  • Jarred-Sumner
  • pfgithub
  • robobun
🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title "fix(console) Async frame deduplication" clearly summarizes the main change—implementing deduplication of duplicate consecutive stack frames in async functions.
Description check ✅ Passed The PR description comprehensively addresses both required template sections with specific technical details about the fix, implementation locations, deduplication logic, and detailed verification methods.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In @test/js/bun/test/stack-format.test.ts:
- Around line 6-7: The test "deep async call stacks should not have duplicates"
mutates Error.stackTraceLimit without restoring it; capture the original value
(e.g., const original = Error.stackTraceLimit) before setting
Error.stackTraceLimit = 50 and restore it at the end of the test (in a finally
block or after hook) so other tests are unaffected; reference
Error.stackTraceLimit and the test name to locate where to save and restore the
value.
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between d530ed9 and 8130803.

📒 Files selected for processing (5)
  • src/bun.js/bindings/ErrorStackTrace.cpp
  • src/bun.js/bindings/ErrorStackTrace.h
  • src/bun.js/bindings/FormatStackTraceForJS.cpp
  • test/js/bun/test/stack-format.test.ts
  • test/regression/issue/22339.test.ts
🧰 Additional context used
📓 Path-based instructions (5)
src/bun.js/bindings/**/*.cpp

📄 CodeRabbit inference engine (CLAUDE.md)

src/bun.js/bindings/**/*.cpp: Create classes in three parts in C++ when there is a public constructor: Foo (JSDestructibleObject), FooPrototype (JSNonFinalObject), and FooConstructor (InternalFunction)
Define properties using HashTableValue arrays in C++ JavaScript class bindings
Add iso subspaces for C++ classes with fields in JavaScript class bindings
Cache structures in ZigGlobalObject for JavaScript class bindings

Files:

  • src/bun.js/bindings/FormatStackTraceForJS.cpp
  • src/bun.js/bindings/ErrorStackTrace.cpp
**/*.test.ts?(x)

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.test.ts?(x): Never use bun test directly - always use bun bd test to run tests with debug build changes
For single-file tests, prefer -e flag over tempDir
For multi-file tests, prefer tempDir and Bun.spawn over single-file tests
Use normalizeBunSnapshot to normalize snapshot output of tests
Never write tests that check for 'panic', 'uncaught exception', or similar strings in test output
Use tempDir from harness to create temporary directories - do not use tmpdirSync or fs.mkdtempSync
When spawning processes in tests, expect stdout before expecting exit code for more useful error messages on test failure
Do not write flaky tests - do not use setTimeout in tests; instead await the condition to be met
Verify tests fail with USE_SYSTEM_BUN=1 bun test <file> and pass with bun bd test <file> - tests are invalid if they pass with USE_SYSTEM_BUN=1
Test files must end with .test.ts or .test.tsx
Avoid shell commands like find or grep in tests - use Bun's Glob and built-in tools instead

Files:

  • test/js/bun/test/stack-format.test.ts
  • test/regression/issue/22339.test.ts
test/**/*.test.ts?(x)

📄 CodeRabbit inference engine (CLAUDE.md)

Always use port: 0 in tests - do not hardcode ports or use custom random port number functions

Files:

  • test/js/bun/test/stack-format.test.ts
  • test/regression/issue/22339.test.ts
test/**/*.test.{ts,js,jsx,tsx,mjs,cjs}

📄 CodeRabbit inference engine (test/CLAUDE.md)

test/**/*.test.{ts,js,jsx,tsx,mjs,cjs}: Use bun bd test <...test file> to run tests with compiled code changes. Do not use bun test as it will not include your changes.
Use bun:test for files ending in *.test.{ts,js,jsx,tsx,mjs,cjs}. For test files without .test extension in test/js/node/test/{parallel,sequential}/*.js, use bun bd <file> instead of bun bd test <file> since they expect exit code 0.
Do not set a timeout on tests. Bun already has timeouts built-in.

Files:

  • test/js/bun/test/stack-format.test.ts
  • test/regression/issue/22339.test.ts
test/regression/issue/*.test.ts

📄 CodeRabbit inference engine (CLAUDE.md)

Place regression tests for specific GitHub issues in test/regression/issue/${issueNumber}.test.ts with real issue numbers only

Files:

  • test/regression/issue/22339.test.ts
🧠 Learnings (44)
📓 Common learnings
Learnt from: alii
Repo: oven-sh/bun PR: 25360
File: src/bake/hmr-runtime-client.ts:240-249
Timestamp: 2025-12-05T19:28:39.534Z
Learning: In Bun's HMR runtime client (src/bake/hmr-runtime-client.ts), when handling error events where `event.error` is falsy, do not wrap `event.message` in a new Error object. Doing so would create misleading stack frames that show the error originating from the HMR client code rather than the actual error location, which hinders debugging.
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: test/CLAUDE.md:0-0
Timestamp: 2026-01-05T23:04:01.518Z
Learning: When spawning Bun processes in tests, use `bunExe` and `bunEnv` from `harness` to ensure the same build of Bun is used and debug logging is silenced.
Learnt from: RiskyMH
Repo: oven-sh/bun PR: 25111
File: packages/bun-types/wasm.d.ts:103-104
Timestamp: 2025-11-27T15:18:21.607Z
Learning: In the Bun repository, autofixci automatically formats code in the main branch, which can cause formatting diffs to appear in pull requests even when the PR author didn't make those changes. These formatting changes from autofixci are expected and not the PR author's responsibility.
📚 Learning: 2025-11-24T18:37:47.899Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: src/bun.js/bindings/v8/AGENTS.md:0-0
Timestamp: 2025-11-24T18:37:47.899Z
Learning: Applies to src/bun.js/bindings/v8/**/<UNKNOWN> : <UNKNOWN>

Applied to files:

  • src/bun.js/bindings/FormatStackTraceForJS.cpp
  • src/bun.js/bindings/ErrorStackTrace.h
📚 Learning: 2025-11-24T18:36:59.706Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: src/bun.js/bindings/v8/CLAUDE.md:0-0
Timestamp: 2025-11-24T18:36:59.706Z
Learning: Applies to src/bun.js/bindings/v8/V8*.cpp : Use JSC::WriteBarrier for heap-allocated references in V8 objects and implement visitChildren() for custom heap objects to support garbage collection

Applied to files:

  • src/bun.js/bindings/FormatStackTraceForJS.cpp
  • src/bun.js/bindings/ErrorStackTrace.cpp
📚 Learning: 2025-11-24T18:36:59.706Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: src/bun.js/bindings/v8/CLAUDE.md:0-0
Timestamp: 2025-11-24T18:36:59.706Z
Learning: Applies to src/bun.js/bindings/v8/src/symbols.txt : Add symbol names without leading underscore to src/symbols.txt for each new V8 API method

Applied to files:

  • src/bun.js/bindings/FormatStackTraceForJS.cpp
  • src/bun.js/bindings/ErrorStackTrace.cpp
  • src/bun.js/bindings/ErrorStackTrace.h
📚 Learning: 2025-12-05T19:28:39.534Z
Learnt from: alii
Repo: oven-sh/bun PR: 25360
File: src/bake/hmr-runtime-client.ts:240-249
Timestamp: 2025-12-05T19:28:39.534Z
Learning: In Bun's HMR runtime client (src/bake/hmr-runtime-client.ts), when handling error events where `event.error` is falsy, do not wrap `event.message` in a new Error object. Doing so would create misleading stack frames that show the error originating from the HMR client code rather than the actual error location, which hinders debugging.

Applied to files:

  • src/bun.js/bindings/FormatStackTraceForJS.cpp
  • src/bun.js/bindings/ErrorStackTrace.cpp
📚 Learning: 2025-11-24T18:36:59.706Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: src/bun.js/bindings/v8/CLAUDE.md:0-0
Timestamp: 2025-11-24T18:36:59.706Z
Learning: Applies to src/bun.js/bindings/v8/src/symbols.dyn : Add symbol names with leading underscore and semicolons in braces to src/symbols.dyn for each new V8 API method

Applied to files:

  • src/bun.js/bindings/FormatStackTraceForJS.cpp
  • src/bun.js/bindings/ErrorStackTrace.cpp
  • src/bun.js/bindings/ErrorStackTrace.h
📚 Learning: 2025-11-24T18:36:59.706Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: src/bun.js/bindings/v8/CLAUDE.md:0-0
Timestamp: 2025-11-24T18:36:59.706Z
Learning: Applies to src/bun.js/bindings/v8/V8*.cpp : Use isolate->currentHandleScope()->createLocal<T>() to create local V8 handles and ensure all V8 values are created within an active handle scope

Applied to files:

  • src/bun.js/bindings/FormatStackTraceForJS.cpp
📚 Learning: 2025-11-24T18:36:59.706Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: src/bun.js/bindings/v8/CLAUDE.md:0-0
Timestamp: 2025-11-24T18:36:59.706Z
Learning: Applies to src/bun.js/bindings/v8/V8*.h : Add BUN_EXPORT visibility attribute to all public V8 API functions to ensure proper symbol export across platforms

Applied to files:

  • src/bun.js/bindings/FormatStackTraceForJS.cpp
  • src/bun.js/bindings/ErrorStackTrace.h
📚 Learning: 2025-11-24T18:36:59.706Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: src/bun.js/bindings/v8/CLAUDE.md:0-0
Timestamp: 2025-11-24T18:36:59.706Z
Learning: Applies to src/bun.js/bindings/v8/V8*.cpp : Create V8 class implementations with .cpp extension following the pattern V8ClassName.cpp that include the header, v8_compatibility_assertions.h, use ASSERT_V8_TYPE_LAYOUT_MATCHES macro, and implement methods using isolate->currentHandleScope()->createLocal<T>() for handle creation

Applied to files:

  • src/bun.js/bindings/FormatStackTraceForJS.cpp
📚 Learning: 2025-12-16T00:21:32.179Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-16T00:21:32.179Z
Learning: Applies to src/bun.js/bindings/**/*.cpp : Add iso subspaces for C++ classes with fields in JavaScript class bindings

Applied to files:

  • src/bun.js/bindings/FormatStackTraceForJS.cpp
  • src/bun.js/bindings/ErrorStackTrace.cpp
  • src/bun.js/bindings/ErrorStackTrace.h
📚 Learning: 2025-11-24T18:36:59.706Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: src/bun.js/bindings/v8/CLAUDE.md:0-0
Timestamp: 2025-11-24T18:36:59.706Z
Learning: Applies to src/bun.js/bindings/v8/V8*.cpp : Use localToJSValue() to convert V8 handles to JSC values and perform JSC operations within V8 method implementations

Applied to files:

  • src/bun.js/bindings/FormatStackTraceForJS.cpp
📚 Learning: 2025-11-19T05:15:22.823Z
Learnt from: pfgithub
Repo: oven-sh/bun PR: 23764
File: src/bun.js/test/timers/FakeTimers.zig:240-0
Timestamp: 2025-11-19T05:15:22.823Z
Learning: In Bun's Zig codebase, `CallFrame.argumentsAsArray(N)` always returns an array of exactly N elements. If fewer than N arguments were passed to the function, the missing slots are filled with `.undefined` JSValue. This means accessing `argumentsAsArray(N)[i]` where i < N is always safe and will never trap—it will simply return `.undefined` if that argument wasn't provided by the caller.

Applied to files:

  • src/bun.js/bindings/FormatStackTraceForJS.cpp
📚 Learning: 2025-11-03T20:40:59.655Z
Learnt from: pfgithub
Repo: oven-sh/bun PR: 24273
File: src/bun.js/bindings/JSValue.zig:545-586
Timestamp: 2025-11-03T20:40:59.655Z
Learning: In Bun's Zig codebase, JSErrors (returned as `bun.JSError!T`) must always be properly handled. Using `catch continue` or `catch { break; }` to silently suppress JSErrors is a bug. Errors should either be explicitly handled or propagated with `try`. This applies to snapshot serializer error handling where Jest's behavior is to throw when serializers throw.

Applied to files:

  • src/bun.js/bindings/FormatStackTraceForJS.cpp
  • test/js/bun/test/stack-format.test.ts
  • src/bun.js/bindings/ErrorStackTrace.h
📚 Learning: 2025-11-24T18:36:59.706Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: src/bun.js/bindings/v8/CLAUDE.md:0-0
Timestamp: 2025-11-24T18:36:59.706Z
Learning: Applies to src/bun.js/bindings/v8/test/v8/v8.test.ts : Add corresponding test cases to test/v8/v8.test.ts using checkSameOutput() function to compare Node.js and Bun output

Applied to files:

  • test/js/bun/test/stack-format.test.ts
  • test/regression/issue/22339.test.ts
📚 Learning: 2026-01-05T23:04:01.518Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: test/CLAUDE.md:0-0
Timestamp: 2026-01-05T23:04:01.518Z
Learning: Applies to test/**/*.test.{ts,js,jsx,tsx,mjs,cjs} : Use `bun bd test <...test file>` to run tests with compiled code changes. Do not use `bun test` as it will not include your changes.

Applied to files:

  • test/js/bun/test/stack-format.test.ts
📚 Learning: 2026-01-05T23:04:01.518Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: test/CLAUDE.md:0-0
Timestamp: 2026-01-05T23:04:01.518Z
Learning: Applies to test/**/*.test.{ts,js,jsx,tsx,mjs,cjs} : Use `bun:test` for files ending in `*.test.{ts,js,jsx,tsx,mjs,cjs}`. For test files without .test extension in test/js/node/test/{parallel,sequential}/*.js, use `bun bd <file>` instead of `bun bd test <file>` since they expect exit code 0.

Applied to files:

  • test/js/bun/test/stack-format.test.ts
  • test/regression/issue/22339.test.ts
📚 Learning: 2025-12-16T00:21:32.179Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-16T00:21:32.179Z
Learning: Applies to **/*.test.ts?(x) : Use `normalizeBunSnapshot` to normalize snapshot output of tests

Applied to files:

  • test/js/bun/test/stack-format.test.ts
  • test/regression/issue/22339.test.ts
📚 Learning: 2025-12-16T00:21:32.179Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-16T00:21:32.179Z
Learning: Applies to **/*.test.ts?(x) : Never use `bun test` directly - always use `bun bd test` to run tests with debug build changes

Applied to files:

  • test/js/bun/test/stack-format.test.ts
📚 Learning: 2025-10-19T02:44:46.354Z
Learnt from: theshadow27
Repo: oven-sh/bun PR: 23798
File: packages/bun-otel/context-propagation.test.ts:1-1
Timestamp: 2025-10-19T02:44:46.354Z
Learning: In the Bun repository, standalone packages under packages/ (e.g., bun-vscode, bun-inspector-protocol, bun-plugin-yaml, bun-plugin-svelte, bun-debug-adapter-protocol, bun-otel) co-locate their tests with package source code using *.test.ts files. This follows standard npm/monorepo patterns. The test/ directory hierarchy (test/js/bun/, test/cli/, test/js/node/) is reserved for testing Bun's core runtime APIs and built-in functionality, not standalone packages.

Applied to files:

  • test/js/bun/test/stack-format.test.ts
  • test/regression/issue/22339.test.ts
📚 Learning: 2026-01-05T23:04:01.518Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: test/CLAUDE.md:0-0
Timestamp: 2026-01-05T23:04:01.518Z
Learning: Applies to test/**/*-fixture.ts : Test files that spawn Bun processes should end in `*-fixture.ts` to identify them as test fixtures rather than tests themselves.

Applied to files:

  • test/js/bun/test/stack-format.test.ts
  • test/regression/issue/22339.test.ts
📚 Learning: 2026-01-05T23:04:01.518Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: test/CLAUDE.md:0-0
Timestamp: 2026-01-05T23:04:01.518Z
Learning: Applies to test/**/*.test.{ts,js,jsx,tsx,mjs,cjs} : Do not set a timeout on tests. Bun already has timeouts built-in.

Applied to files:

  • test/js/bun/test/stack-format.test.ts
  • test/regression/issue/22339.test.ts
📚 Learning: 2025-12-16T00:21:32.179Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-16T00:21:32.179Z
Learning: Applies to **/*.test.ts?(x) : Never write tests that check for 'panic', 'uncaught exception', or similar strings in test output

Applied to files:

  • test/js/bun/test/stack-format.test.ts
  • test/regression/issue/22339.test.ts
📚 Learning: 2025-12-16T00:21:32.179Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-16T00:21:32.179Z
Learning: Applies to **/*.test.ts?(x) : For multi-file tests, prefer `tempDir` and `Bun.spawn` over single-file tests

Applied to files:

  • test/js/bun/test/stack-format.test.ts
  • test/regression/issue/22339.test.ts
📚 Learning: 2025-10-08T13:48:02.430Z
Learnt from: Jarred-Sumner
Repo: oven-sh/bun PR: 23373
File: test/js/bun/tarball/extract.test.ts:107-111
Timestamp: 2025-10-08T13:48:02.430Z
Learning: In Bun's test runner, use `expect(async () => { await ... }).toThrow()` to assert async rejections. Unlike Jest/Vitest, Bun does not require `await expect(...).rejects.toThrow()` - the async function wrapper with `.toThrow()` is the correct pattern for async error assertions in Bun tests.

Applied to files:

  • test/js/bun/test/stack-format.test.ts
  • test/regression/issue/22339.test.ts
📚 Learning: 2025-11-06T00:58:23.965Z
Learnt from: markovejnovic
Repo: oven-sh/bun PR: 24417
File: test/js/bun/spawn/spawn.test.ts:903-918
Timestamp: 2025-11-06T00:58:23.965Z
Learning: In Bun test files, `await using` with spawn() is appropriate for long-running processes that need guaranteed cleanup on scope exit or when explicitly testing disposal behavior. For short-lived processes that exit naturally (e.g., console.log scripts), the pattern `const proc = spawn(...); await proc.exited;` is standard and more common, as evidenced by 24 instances vs 4 `await using` instances in test/js/bun/spawn/spawn.test.ts.

Applied to files:

  • test/js/bun/test/stack-format.test.ts
  • test/regression/issue/22339.test.ts
📚 Learning: 2025-09-20T00:58:38.042Z
Learnt from: markovejnovic
Repo: oven-sh/bun PR: 22568
File: test/js/valkey/valkey.test.ts:561-564
Timestamp: 2025-09-20T00:58:38.042Z
Learning: For test/js/valkey/valkey.test.ts, do not comment on synchronous throw assertions for async Redis methods (like ctx.redis.set(), ctx.redis.unsubscribe(), etc.) - Bun's Redis client implementation differs from Node.js and can throw synchronously even for async methods. The maintainer has explicitly requested to stop looking at this error pattern.

Applied to files:

  • test/js/bun/test/stack-format.test.ts
📚 Learning: 2025-09-20T03:39:41.770Z
Learnt from: pfgithub
Repo: oven-sh/bun PR: 22534
File: test/regression/issue/21830.fixture.ts:14-63
Timestamp: 2025-09-20T03:39:41.770Z
Learning: Bun's test runner supports async describe callbacks, unlike Jest/Vitest where describe callbacks must be synchronous. The syntax `describe("name", async () => { ... })` is valid in Bun.

Applied to files:

  • test/js/bun/test/stack-format.test.ts
  • test/regression/issue/22339.test.ts
📚 Learning: 2025-12-16T00:21:32.179Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-16T00:21:32.179Z
Learning: Applies to **/*.test.ts?(x) : Verify tests fail with `USE_SYSTEM_BUN=1 bun test <file>` and pass with `bun bd test <file>` - tests are invalid if they pass with USE_SYSTEM_BUN=1

Applied to files:

  • test/js/bun/test/stack-format.test.ts
📚 Learning: 2025-12-16T00:21:32.179Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-16T00:21:32.179Z
Learning: Applies to test/regression/issue/*.test.ts : Place regression tests for specific GitHub issues in `test/regression/issue/${issueNumber}.test.ts` with real issue numbers only

Applied to files:

  • test/regression/issue/22339.test.ts
📚 Learning: 2026-01-05T23:04:01.518Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: test/CLAUDE.md:0-0
Timestamp: 2026-01-05T23:04:01.518Z
Learning: Organize regression tests for specific issues in `/test/regression/issue/${issueNumber}.test.ts`. Do not place regression tests in the regression directory if there is no associated issue number.

Applied to files:

  • test/regression/issue/22339.test.ts
📚 Learning: 2025-09-20T00:57:56.685Z
Learnt from: markovejnovic
Repo: oven-sh/bun PR: 22568
File: test/js/valkey/valkey.test.ts:268-276
Timestamp: 2025-09-20T00:57:56.685Z
Learning: For test/js/valkey/valkey.test.ts, do not comment on synchronous throw assertions for async Redis methods like ctx.redis.set() - the maintainer has explicitly requested to stop looking at this error pattern.

Applied to files:

  • test/regression/issue/22339.test.ts
📚 Learning: 2025-12-16T00:21:32.179Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-16T00:21:32.179Z
Learning: Applies to **/*.test.ts?(x) : Do not write flaky tests - do not use `setTimeout` in tests; instead await the condition to be met

Applied to files:

  • test/regression/issue/22339.test.ts
📚 Learning: 2025-12-16T00:21:32.179Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-16T00:21:32.179Z
Learning: Applies to **/*.test.ts?(x) : When spawning processes in tests, expect stdout before expecting exit code for more useful error messages on test failure

Applied to files:

  • test/regression/issue/22339.test.ts
📚 Learning: 2025-11-24T18:36:59.706Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: src/bun.js/bindings/v8/CLAUDE.md:0-0
Timestamp: 2025-11-24T18:36:59.706Z
Learning: Applies to src/bun.js/bindings/v8/test/v8/v8-module/main.cpp : Create test functions in test/v8/v8-module/main.cpp that take FunctionCallbackInfo<Value> parameter, use the test V8 API, print results for comparison with Node.js, and return Undefined

Applied to files:

  • test/regression/issue/22339.test.ts
📚 Learning: 2025-09-30T22:53:19.887Z
Learnt from: pfgithub
Repo: oven-sh/bun PR: 23117
File: src/bun.js/test/snapshot.zig:265-276
Timestamp: 2025-09-30T22:53:19.887Z
Learning: In Bun's snapshot testing (src/bun.js/test/snapshot.zig), multiple inline snapshots at the same line and column (same call position) must have identical values. However, multiple inline snapshots on the same line at different columns are allowed to have different values. The check is position-specific (line+col), not line-wide.

Applied to files:

  • test/regression/issue/22339.test.ts
📚 Learning: 2025-10-18T05:23:24.403Z
Learnt from: theshadow27
Repo: oven-sh/bun PR: 23798
File: test/js/bun/telemetry-server.test.ts:91-100
Timestamp: 2025-10-18T05:23:24.403Z
Learning: In the Bun codebase, telemetry tests (test/js/bun/telemetry-*.test.ts) should focus on telemetry API behavior: configure/disable/isEnabled, callback signatures and invocation, request ID correlation, and error handling. HTTP protocol behaviors like status code normalization (e.g., 200 with empty body → 204) should be tested in HTTP server tests (test/js/bun/http/), not in telemetry tests. Keep separation of concerns: telemetry tests verify the telemetry API contract; HTTP tests verify HTTP semantics.

Applied to files:

  • test/regression/issue/22339.test.ts
📚 Learning: 2025-11-24T18:36:59.706Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: src/bun.js/bindings/v8/CLAUDE.md:0-0
Timestamp: 2025-11-24T18:36:59.706Z
Learning: Applies to src/bun.js/bindings/v8/src/napi/napi.zig : For each new V8 C++ method, add both GCC/Clang and MSVC mangled symbol names to the V8API struct in src/napi/napi.zig using extern fn declarations

Applied to files:

  • src/bun.js/bindings/ErrorStackTrace.cpp
  • src/bun.js/bindings/ErrorStackTrace.h
📚 Learning: 2025-12-23T06:50:41.142Z
Learnt from: Jarred-Sumner
Repo: oven-sh/bun PR: 25429
File: src/bun.js/bindings/helpers.h:422-422
Timestamp: 2025-12-23T06:50:41.142Z
Learning: In Bun's C++ bindings (src/bun.js/bindings/helpers.h and similar files), when returning an empty JSC::Identifier and a VM is accessible, prefer using `vm.propertyNames->emptyIdentifier` over constructing with `JSC::Identifier(JSC::Identifier::EmptyIdentifierFlag::EmptyIdentifier)`. The cached identifier from the VM's property names table is more efficient and consistent with WebKit upgrade patterns.

Applied to files:

  • src/bun.js/bindings/ErrorStackTrace.cpp
📚 Learning: 2025-12-16T00:21:32.179Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-16T00:21:32.179Z
Learning: Applies to src/bun.js/bindings/**/*.cpp : Cache structures in ZigGlobalObject for JavaScript class bindings

Applied to files:

  • src/bun.js/bindings/ErrorStackTrace.cpp
  • src/bun.js/bindings/ErrorStackTrace.h
📚 Learning: 2025-11-24T18:37:11.466Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: src/js/CLAUDE.md:0-0
Timestamp: 2025-11-24T18:37:11.466Z
Learning: Applies to src/js/{builtins,node,bun,thirdparty,internal}/**/*.{ts,js} : Use JSC intrinsics (prefixed with `$`) such as `$Array.from()`, `$isCallable()`, and `$newArrayWithSize()` for performance-critical operations

Applied to files:

  • src/bun.js/bindings/ErrorStackTrace.cpp
📚 Learning: 2025-12-16T00:21:32.179Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-16T00:21:32.179Z
Learning: Applies to src/bun.js/bindings/**/*.cpp : Create classes in three parts in C++ when there is a public constructor: Foo (JSDestructibleObject), FooPrototype (JSNonFinalObject), and FooConstructor (InternalFunction)

Applied to files:

  • src/bun.js/bindings/ErrorStackTrace.cpp
📚 Learning: 2025-11-24T18:37:11.466Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: src/js/CLAUDE.md:0-0
Timestamp: 2025-11-24T18:37:11.466Z
Learning: Applies to src/js/{builtins,node,bun,thirdparty,internal}/**/*.ts : Builtin functions must include `this` parameter typing in TypeScript to enable direct method binding in C++

Applied to files:

  • src/bun.js/bindings/ErrorStackTrace.h
📚 Learning: 2025-10-01T21:59:54.571Z
Learnt from: taylordotfish
Repo: oven-sh/bun PR: 23169
File: src/bun.js/bindings/webcore/JSDOMConvertEnumeration.h:47-74
Timestamp: 2025-10-01T21:59:54.571Z
Learning: In the new bindings generator (bindgenv2) for `src/bun.js/bindings/webcore/JSDOMConvertEnumeration.h`, the context-aware enumeration conversion overloads intentionally use stricter validation (requiring `value.isString()` without ToString coercion), diverging from Web IDL semantics. This is a design decision documented in comments.

Applied to files:

  • src/bun.js/bindings/ErrorStackTrace.h
📚 Learning: 2025-12-23T06:50:31.577Z
Learnt from: Jarred-Sumner
Repo: oven-sh/bun PR: 25429
File: src/bun.js/bindings/helpers.h:422-422
Timestamp: 2025-12-23T06:50:31.577Z
Learning: In Bun's C++ bindings, when returning an empty JSC::Identifier and a VM is accessible, prefer using vm.propertyNames->emptyIdentifier over constructing with JSC::Identifier(JSC::Identifier::EmptyIdentifierFlag::EmptyIdentifier). The cached identifier from the VM's property names table is more efficient and consistent with WebKit upgrade patterns. Apply this guidance to src/bun.js/bindings/helpers.h and similar header files in the same bindings directory (i.e., any file that constructs an EmptyIdentifier).

Applied to files:

  • src/bun.js/bindings/ErrorStackTrace.h
🧬 Code graph analysis (3)
src/bun.js/bindings/FormatStackTraceForJS.cpp (2)
src/bun.js/bindings/ZigException.cpp (2)
  • frame (264-410)
  • frame (264-264)
src/bun.js/bindings/ErrorStackTrace.cpp (10)
  • isImplementationVisibilityPrivate (43-68)
  • isImplementationVisibilityPrivate (43-43)
  • isImplementationVisibilityPrivate (70-94)
  • isImplementationVisibilityPrivate (70-70)
  • isInternalBuiltinFrame (223-231)
  • isInternalBuiltinFrame (223-223)
  • isConsecutiveDuplicateAsyncFrame (125-164)
  • isConsecutiveDuplicateAsyncFrame (125-125)
  • isConsecutiveDuplicateAsyncFrame (166-220)
  • isConsecutiveDuplicateAsyncFrame (166-166)
src/bun.js/bindings/ErrorStackTrace.cpp (1)
src/bun.js/bindings/ZigException.cpp (2)
  • frame (264-410)
  • frame (264-264)
src/bun.js/bindings/ErrorStackTrace.h (1)
src/bun.js/bindings/ErrorStackTrace.cpp (6)
  • isConsecutiveDuplicateAsyncFrame (125-164)
  • isConsecutiveDuplicateAsyncFrame (125-125)
  • isConsecutiveDuplicateAsyncFrame (166-220)
  • isConsecutiveDuplicateAsyncFrame (166-166)
  • isInternalBuiltinFrame (223-231)
  • isInternalBuiltinFrame (223-223)
🔇 Additional comments (18)
src/bun.js/bindings/ErrorStackTrace.h (1)

213-214: LGTM!

The new function declarations are consistent with their implementations in ErrorStackTrace.cpp and appropriately placed in the Zig namespace alongside the existing isImplementationVisibilityPrivate overloads.

src/bun.js/bindings/FormatStackTraceForJS.cpp (3)

150-174: LGTM - Efficient index-based filtering.

The filtering approach using filteredIndices is well-designed:

  • Avoids copying frames by storing indices
  • Correctly tracks prevUserFrame for duplicate detection
  • Applies filters in the right order (private visibility → builtin → duplicate)

One observation: This file appears to have local implementations of isConsecutiveDuplicateAsyncFrame (per the relevant code snippets at lines 124-219) that are similar to those in ErrorStackTrace.cpp. If these are intentionally different overloads (one for StackVisitor, one for StackFrame), this is fine. Otherwise, consider consolidating.


203-203: Correctly updated to use filtered frames.

The SyntaxError handling now properly uses the first filtered frame instead of the raw first frame, which aligns with the deduplication behavior.


257-265: LGTM - Safe indexed access.

The assertion and loop correctly use the filtered indices. The bounds are safe since i < framesCount where framesCount = filteredIndices.size().

test/regression/issue/22339.test.ts (5)

1-16: Well-structured regression test with clear issue reference.

The file is correctly placed in test/regression/issue/ with a real GitHub issue number. The getCallSites() helper properly captures stack traces using Error.prepareStackTrace.


18-35: Good duplicate detection helper.

The hasConsecutiveDuplicates logic correctly mirrors the C++ implementation in compareFramesForDuplicates:

  • Same function name
  • Same file name
  • Line numbers within 1 line of each other

37-62: Comprehensive async chain test.

Good test coverage that verifies:

  • No consecutive duplicates in the captured stack
  • Expected function names are present
  • Each function appears exactly once (for non-recursive case)

89-110: Important recursion test.

This test correctly validates that actual recursive calls are preserved (multiple frames allowed) while consecutive duplicates (JSC artifacts) are filtered. The comment at lines 94-95 and 101-102 clearly explains the distinction.


166-191: Good baseline test for sync functions.

Ensures the deduplication logic doesn't inadvertently affect regular synchronous function stacks.

test/js/bun/test/stack-format.test.ts (3)

25-43: Good comprehensive assertions.

The test properly validates:

  • All 5 levels are present in the stack
  • No consecutive duplicate lines
  • No asyncFunctionResume internal frames leak through

46-65: Important recursive async test.

Correctly validates that actual recursion preserves multiple frames while filtering duplicates.


139-157: Good Promise rejection coverage.

Tests the edge case of Promise.reject() stack traces, ensuring deduplication works correctly for rejected promises.

src/bun.js/bindings/ErrorStackTrace.cpp (6)

96-123: Well-documented duplicate detection logic.

The comment at lines 117-120 clearly explains the rationale for the 1-line threshold for detecting JSC's duplicate async frames. The logic correctly handles edge cases (empty names, empty URLs).


125-164: Potential inconsistency in function name retrieval.

The StackVisitor overload retrieves currentFunctionName via visitor->functionName() (line 131), but for prevFrame it only uses jsFunc->name(vm) (line 136).

In contrast, the StackFrame overload (lines 178-194) tries Zig::functionName(vm, codeBlock) first, then falls back to jsFunc->name(vm).

This asymmetry could cause missed duplicate detection if the function name is stored differently (e.g., in the code block's ecmaName vs the JSFunction's name property).

Consider aligning the name retrieval logic:

♻️ Suggested alignment
     WTF::String prevFunctionName;
 
     if (auto* callee = prevFrame->callee()) {
         if (auto* jsFunction = jsDynamicCast<JSFunction*>(callee)) {
-            prevFunctionName = jsFunction->name(vm);
+            if (auto* codeBlock = prevFrame->codeBlock()) {
+                prevFunctionName = Zig::functionName(vm, codeBlock);
+            }
+            if (prevFunctionName.isEmpty()) {
+                prevFunctionName = jsFunction->name(vm);
+            }
         }
     }

166-220: Consistent name retrieval in StackFrame overload.

This overload correctly tries Zig::functionName(vm, codeBlock) first before falling back to jsFunc->name(vm) for both current and previous frames. This is the correct pattern.


222-231: Clean builtin detection.

Simple and effective check using the unlinked code block's isBuiltinFunction() method.


243-265: Correct filtering in fromExisting.

The filtering logic properly:

  1. Skips private visibility frames
  2. Skips internal builtin frames
  3. Skips consecutive duplicate async frames
  4. Tracks prevUserFrame correctly for duplicate detection

367-427: Comprehensive frame tracking in getFramesForCaller.

The dual-pointer approach (prevFrame and prevUserFrame) is well-designed:

  • prevFrame tracks the last added frame for general purposes
  • prevUserFrame tracks the last non-builtin frame specifically for duplicate detection

Lines 416-418 correctly update prevUserFrame only for non-builtin frames.

Comment on lines +6 to +7
test("deep async call stacks should not have duplicates", async () => {
Error.stackTraceLimit = 50; // Increase to see all frames

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Consider saving and restoring Error.stackTraceLimit.

Setting Error.stackTraceLimit = 50 without restoring the original value could affect subsequent tests in the same test run.

♻️ Suggested fix
 test("deep async call stacks should not have duplicates", async () => {
+  const originalLimit = Error.stackTraceLimit;
   Error.stackTraceLimit = 50; // Increase to see all frames
+  try {

And at the end of the test:

     // No asyncFunctionResume
     expect(stack).not.toContain("asyncFunctionResume");
   }
+  } finally {
+    Error.stackTraceLimit = originalLimit;
+  }
 });
🤖 Prompt for AI Agents
In @test/js/bun/test/stack-format.test.ts around lines 6 - 7, The test "deep
async call stacks should not have duplicates" mutates Error.stackTraceLimit
without restoring it; capture the original value (e.g., const original =
Error.stackTraceLimit) before setting Error.stackTraceLimit = 50 and restore it
at the end of the test (in a finally block or after hook) so other tests are
unaffected; reference Error.stackTraceLimit and the test name to locate where to
save and restore the value.

@Jarred-Sumner Jarred-Sumner left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for this PR.

This should be fixed in WebKit upstream and not in Bun - and it will be fixed in the next upgrade

@gavinhailey

gavinhailey commented Jan 13, 2026 •

Copy link
Copy Markdown
Author

That makes sense, felt a bit patchy implementing it, looks like this WebKit PR fixed it.
Thanks for pointing me in the right direction.

@gavinhailey
gavinhailey deleted the deduplicate-async-stack-frames branch January 13, 2026 05:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants