Skip to content

console: guard AggregateError .errors recursion (cycle, depth, tampered property) - #35825

Closed
robobun wants to merge 11 commits into
mainfrom
farm/6e616f86/aggregate-error-cycle-guard
Closed

console: guard AggregateError .errors recursion (cycle, depth, tampered property)#35825
robobun wants to merge 11 commits into
mainfrom
farm/6e616f86/aggregate-error-cycle-guard

Conversation

@robobun

@robobun robobun commented Jul 25, 2026

Copy link
Copy Markdown
Collaborator

Reproduction

All of the following segfault (silent 139, ASan prints nothing) on every sink that reaches the native error printer (console.log/console.error, Bun.inspect, uncaught throw, unhandled rejection, uncaughtException handler, and the in-Worker uncaught path):

// self-cycle
const ae = new AggregateError([], "self"); ae.errors = [ae]; console.log(ae);

// mutual cycle
const a = new AggregateError([], "A"), b = new AggregateError([], "B");
a.errors = [b]; b.errors = [a]; console.log(a);

// deep chain (no cycle)
let e = new AggregateError([], "leaf");
for (let i = 0; i < 3000; i++) e = new AggregateError([e], "");
console.log(e);

// deleted / non-iterable / accessor .errors
const d = new AggregateError([new Error("x")], "msg"); delete d.errors; console.log(d);

And a very deep plain cause chain segfaults on the uncaught/reject sinks only:

let e = new Error("leaf");
for (let i = 0; i < 3000; i++) e = new Error("", { cause: e });
throw e;

Cause

The AggregateError branch of print_errorlike_object walks .errors via for_each -> agg_iter -> print_errorlike_object with no visited set, no stack_check, and no validation of the getDirect(errors) result. At ~8 KB of native stack per level the recursion reaches the guard page. The cause chain already has both guards in print_error_instance_js, but the run_error_handler formatter never seated StackCheck::init(), so that depth guard was inert on the uncaught-throw / unhandled-reject sinks.

Fix

In the aggregate branch:

  1. stack_check.is_safe_to_recurse() at entry, writing [AggregateError: nesting too deep] and returning when it fails.
  2. The existing formatter.map visited pool (same get_or_put / found_existing -> [Circular] / remove idiom the cause-chain uses) keyed on the AggregateError object, inserted before printing and removed after.
  3. Only iterate when .errors is an object; clear any exception for_each raises. This covers deleted, accessor, and non-iterable .errors.

With the guards in place the branch now falls through to print the AggregateError's own name/message/stack before its children, so an uncaught new AggregateError([inner], "outer") renders the AggregateError: outer header (previously only the inner errors were printed).

StackCheck::init() is seated on both run_error_handler formatter sites (VirtualMachine::print_exception and jsc_hooks::print_exception), making the existing cause-chain depth guard effective on those two sinks.

Verification

test/js/bun/util/inspect-error-cycle.test.ts spawns a child per {shape} x {sink} cell (7 shapes x 6 sinks, plus 6 deep-aggregate cells and 2 deep-cause cells, plus 4 output assertions) and asserts no signal. 37/48 cells crash on current main; all 48 pass with this change.

Supersedes the partial fixes in #34892 (depth only), #35820 (self-ref only), #35174 (header only), and #31988 (tampered property only).

Fixes #21528


[review] gate passed · iteration 4 · 4 files touched

fails on main (without fix)
ASAN without fix: 38 FAILED
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/js/bun/util/inspect-error-cycle.test.ts test/regression/issue/jsx-template-string-crash.test.ts
bun test v1.4.0 (055856f5b)

test/regression/issue/jsx-template-string-crash.test.ts:
13 |     stderr: "pipe",
14 |     stdout: "pipe",
15 |   });
16 | 
17 |   expect(exitCode).toBe(1);
18 |   expect(normalizeBunSnapshot(stderr.toString().replace(/(Bun v.*)$/gm, ""))).toMatchInlineSnapshot(`
                                                                                   ^
error: expect(received).toMatchInlineSnapshot(expected)

  
- "AggregateError: 2 errors building "<cwd>/[eval]"
- 1 | export function x(){return<div a=``/>}
+ "1 | export function x(){return<div a=``/>}
                                       ^
  error: Expected "{" but found "`"
      at <cwd>/[eval]:1:34
  
  1 | export function x(){return<div a=``/>}
                                        ^
  error: Unterminated string literal
      at <cwd>/[eval]:1:35"
  

- Expected  - 2
+ Received  + 1

      at <anonymous> (/workspace/bun/test/regression/issue/jsx-template-s
... (truncated)

release without fix: all passed
bun test v1.4.0-canary.1 (de1b576bf)

test/regression/issue/jsx-template-string-crash.test.ts:
(pass) JSX lexer should not crash with slice bounds issues [9.53ms]
(pass) #30959 JSX attribute with invalid '(' value parses cleanly in debug builds [7.60ms]

test/js/bun/util/inspect-error-cycle.test.ts:
(pass) error-graph cycles do not crash the printer > self-cycle x console.error [19.23ms]
(pass) error-graph cycles do not crash the printer > self-cycle x console.log [21.54ms]
(pass) error-graph cycles do not crash the printer > self-cycle x uncaught-throw [20.60ms]
(pass) error-graph cycles do not crash the printer > self-cycle x unhandled-reject [19.97ms]
(pass) error-graph cycles do not crash the printer > self-cycle x uncaughtException-handler [19.85ms]
(pass) error-graph cycles do not crash the printer > self-cycle x Bun.inspect [22.33ms]
(pass) error-graph cycles do not crash the printer > mutual-cycle x Bun.inspect [21.43ms]
(pass) error-graph cycles do not crash the printer > mutual-cycle x console.error [23.19ms]
(pass) error-graph cycles do not crash the printer > mutual-cycle x console.log [24.10ms]
(pass) error-graph cycles do not crash the printer > mutual
... (truncated)
passes on PR (with fix)
ASAN with fix: all passed
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/js/bun/util/inspect-error-cycle.test.ts test/regression/issue/jsx-template-string-crash.test.ts
bun test v1.4.0 (055856f5b)

test/regression/issue/jsx-template-string-crash.test.ts:
(pass) JSX lexer should not crash with slice bounds issues [318.97ms]
(pass) #30959 JSX attribute with invalid '(' value parses cleanly in debug builds [297.49ms]

test/js/bun/util/inspect-error-cycle.test.ts:
(pass) error-graph cycles do not crash the printer > self-cycle x console.log [307.68ms]
(pass) error-graph cycles do not crash the printer > self-cycle x console.error [295.70ms]
(pass) error-graph cycles do not crash the printer > self-cycle x uncaught-throw [296.38ms]
(pass) error-graph cycles do not crash the printer > self-cycle x unhandled-reject [301.33ms]
(pass) error-graph cycles do not crash the printer > self-cycle x Bun.inspect [355.46ms]
(pass) error-graph cycles do not crash the printer > self-cycle x uncaughtException-handler [281.07ms]
(pass) error-graph cycles do not crash the printer > mutual-cycle x console.log [290.64ms]
(pass
... (truncated)

release with fix: all passed
$ bun scripts/build.ts --profile=release
[configured] bun-profile → bun (stripped) in 767ms (unchanged)
ninja: Entering directory `/workspace/bun/build/release'
[1/32] gen cpp.rs (cppbind)
[2/32] gen generated_host_exports.rs
generated_host_exports.rs: 94 exports (host=3, lazy=10, generic=81, rust=0); 240 extern-C blocks audited
[3/32] gen ZigGeneratedClasses.{cpp,h,rs}
Found 2 classes from /workspace/bun/src/jsc/resolve_message.classes.ts
  - ResolveMessage (13 fields)
  - BuildMessage (10 fields)
Found 1 classes from /workspace/bun/src/runtime/api/Archive.classes.ts
  - Archive (4 fields, 1 class fields)
Found 2 classes from /workspace/bun/src/runtime/api/BunObject.classes.ts
  - ResourceUsage (8 fields)
  - Subprocess (20 fields)
Found 1 classes from /workspace/bun/src/runtime/api/cron.classes.ts
  - CronJob (5 fields)
Found 3 classes from /workspace/bun/src/runtime/api/filesystem_router.classes.ts
  - FileSystemRouter (5 fields)
  - FrameworkFileSystemRouter (2 fields)
  - MatchedRoute (8 fields)
Found 1 classes from /workspace/bun/src/runtime/api/Glob.classes.ts
  - Glob (5 fields)
Found 1 classes from /workspace/bun/src/runtime/api/h2.classes.ts
  - H2FrameP
... (truncated)
diff hotspot
src/jsc/VirtualMachine.rs                          |  95 ++++++++----
 src/runtime/jsc_hooks.rs                           |   1 +
 test/js/bun/util/inspect-error-cycle.test.ts       | 172 +++++++++++++++++++++
 .../issue/jsx-template-string-crash.test.ts        |   6 +-
 4 files changed, 246 insertions(+), 28 deletions(-)

gate history · 7 passed · 2 rejected · iteration 4

evidence per changed file
file                                                     reads  edits  tests
src/jsc/VirtualMachine.rs                                   16     11      0
src/runtime/jsc_hooks.rs                                     1      1      0
test/js/bun/util/inspect-error-cycle.test.ts                 4      8      0
test/regression/issue/jsx-template-string-crash.test.ts      1      0      0

…ck check, and property validation

The AggregateError branch of print_errorlike_object walked .errors via
for_each -> agg_iter -> print_errorlike_object with no visited set, no
stack_check, and no validation of the getDirect result. A self/mutual
cycle, a 3000-deep chain of aggregates, or a deleted/accessor .errors
property all reached the stack guard page (silent SIGSEGV) on every
sink that uses the native error printer (console.log/error, Bun.inspect,
uncaught throw, unhandled rejection, uncaughtException handler).

The cause-chain printer already has both guards (stack_check at
print_error_instance_js and the formatter.map [Circular] check), so
the fix reuses the same visited-pool idiom keyed on the AggregateError
object identity, adds the stack_check at the top of the branch, and
only iterates when .errors is an object (clearing any exception
for_each may raise).

Seating StackCheck::init() on the two run_error_handler formatter
sites makes the existing cause-chain depth guard effective on the
uncaught-throw / unhandled-reject sinks, where Formatter::new left it
defaulted and therefore always-true.

With the guards in place the branch now falls through to print the
AggregateError's own name/message/stack before its children, so an
uncaught AggregateError shows its own message instead of only its
members.
@coderabbitai

coderabbitai Bot commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@robobun, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 13 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 8d50ba1e-780a-4f9c-b792-b06d9ffc0356

📥 Commits

Reviewing files that changed from the base of the PR and between 916492f and 055856f.

📒 Files selected for processing (4)
  • src/jsc/VirtualMachine.rs
  • src/runtime/jsc_hooks.rs
  • test/js/bun/util/inspect-error-cycle.test.ts
  • test/regression/issue/jsx-template-string-crash.test.ts

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

@robobun

robobun commented Jul 25, 2026

Copy link
Copy Markdown
Collaborator Author

Status: diff is green. inspect-error-cycle.test.ts (47 cells) and the updated jsx-template-string-crash snapshots pass on every lane that built in #82045. Remaining CI red is unrelated to this diff:

  • s3.test.ts on alpine-aarch64: HTTP 500 from R2 after ~20s (external service)
  • four [flaky] retries: test-fastutf8stream-reopen, complex-workspace, test-http-server-connections-checking-leak, no-orphans
  • darwin-aarch64 build-bun agent timeout

Locally 47/47 pass on debug+ASAN (~7s) and release (~0.4s); 32/47 segfault on an unfixed build. Verified under BUN_JSC_validateExceptionChecks=1. Ready for review.

@robobun

robobun commented Jul 25, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 10:15 PM PT - Jul 25th, 2026

@robobun, your commit 055856f has 1 failures in Build #82045 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 35825

That installs a local version of the PR into your bun-35825 executable, so you can run:

bun-35825 --bun

Comment thread src/jsc/VirtualMachine.rs Outdated
Comment thread src/jsc/VirtualMachine.rs Outdated
Comment thread src/jsc/VirtualMachine.rs Outdated
Comment thread src/jsc/VirtualMachine.rs
Comment thread src/jsc/VirtualMachine.rs Outdated
@github-actions

Copy link
Copy Markdown
Contributor

Found 2 issues this PR may fix:

  1. print AggregateError more distinctly #21528 - PR adds AggregateError header (name/message/stack) printing for uncaught exceptions, which is exactly what this issue requests
  2. Bun prints multi-level Error.cause chain as separate errors instead of preserving Node-style nested stack trace #32343 - PR adds circular reference detection ([Circular]), depth limit guards, and initializes StackCheck for uncaught-throw/rejection sinks, addressing the circular cause chain crash

If this is helpful, copy the block below into the PR description to auto-close these issues on merge.

Fixes #21528
Fixes #32343

🤖 Generated with Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

This PR may be a duplicate of:

  1. Guard print_errorlike_object against unbounded AggregateError recursion #34892 - Adds the same stack depth guard for deep AggregateError recursion in print_errorlike_object
  2. Guard AggregateError .errors printing against self-reference #35820 - Adds the same circular reference guard for cyclic AggregateError .errors
  3. Print the AggregateError header before iterating its members #35174 - Prints the AggregateError header before children in the same code path
  4. Fix crashes printing AggregateError with tampered or cyclic errors (uncaught, console.log, Bun.inspect) #31988 - Guards against tampered/deleted .errors property causing segfault
  5. console: guard throwing Event/AggregateError property reads in the native formatters #35816 - Guards throwing AggregateError .errors reads with is_object() check and clear_exception()
  6. console: cap Map/Set entries at 100 in the native formatter #35826 - Swallows throwing accessors on AggregateError .errors with same validation approach
  7. Fix: print AggregateError name and message before child errors #35050 - Prints AggregateError name and message before child errors (same reorder as this PR)

🤖 Generated with Claude Code

Comment thread test/js/bun/util/inspect-error-cycle.test.ts Outdated

@claude claude 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.

No bugs found, but this restructures control flow in the native error printer and changes user-visible output (AggregateError now prints its own header before children on uncaught throw — the #21528 fix), so it's worth a human look. There are also 7 open PRs listed as duplicates on the same code path; a maintainer should pick which lands and close the rest.

What was reviewed:

  • Visited-map / get_or_put / remove idiom matches the existing cause-chain guard (VirtualMachine.rs:6141) and Formatter cycle check (ConsoleObject.rs:3363).
  • StackCheck::init() seating matches the pattern at ConsoleObject.rs:727/1439/1502/5974; Formatter::new leaves it as default() (always-pass), so seating it here is required.
  • get_errors_property is getDirect (nothrow); is_object() gates out empty and GetterSetter; for_each errors are cleared.
  • Ruled out double-printing of .errors (property-dump path vs. for_each iteration are on separate branches).
Extended reasoning...

Overview

The PR guards the AggregateError branch of print_errorlike_object in src/jsc/VirtualMachine.rs with a stack-depth check, a visited-set cycle check (reusing formatter.map), and validation of the .errors property before iterating it. It also moves the print_error_from_maybe_private_data call above the children loop so an uncaught AggregateError now prints its own name/message/stack (fixing #21528). Two Formatter::new sites (VirtualMachine::print_exception, jsc_hooks::print_exception) now seat StackCheck::init() so the pre-existing cause-chain depth guard actually fires on the uncaught/reject sinks. A new 48-cell subprocess test matrix covers {self-cycle, mutual-cycle, deleted/accessor/non-iterable .errors, mixed agg+cause, deep-aggregate, deep-cause} × {console.log, console.error, Bun.inspect, uncaught-throw, unhandled-reject, uncaughtException-handler}.

Security risks

None. The change is defensive (prevents user-triggerable stack-overflow segfaults in the error printer). No new user input reaches native code that didn't before; get_errors_property remains getDirect (no getter invocation), and any exception raised inside for_each is now cleared rather than swallowed via let _ =.

Level of scrutiny

Moderate-to-high. The error printer runs on every uncaught exception, unhandled rejection, and console.error; a regression here degrades every crash report. The visited-map and stack-check additions are straight copies of existing idioms in the same file, which reduces risk. The larger judgment call is the output-format change: AggregateError uncaught output will now include an extra header line, which is the desired fix for #21528 but is a user-visible change that could touch downstream snapshot expectations. That, plus the 7 open competing PRs on the same lines (#34892, #35820, #35174, #31988, #35816, #35826, #35050), makes this a maintainer decision rather than a mechanical approve.

Other factors

My prior nit (serial describe on the depth block) was addressed in b7b1a39; the file now runs ~9s under debug+ASAN. The comment-cop bot flags were on pre-existing comments that moved in the diff (author confirmed) and are all resolved. The evidence block's "release without fix: all passed" label is confusing (the hash shown is a PR commit), but robobun separately confirmed 37/48 cells segfault under USE_SYSTEM_BUN=1, which is the load-bearing fails-on-main proof.

Comment thread src/jsc/VirtualMachine.rs
…g for_each with a pending exception

Skip the .errors iteration when the header print already tripped the
inner stack guard (formatter.failed or pending exception), and only
clear a for_each error when formatter.failed is unset. Bun.inspect /
console.* on a very deep AggregateError now throws RangeError like it
does for a deep cause chain.
Comment thread src/jsc/VirtualMachine.rs Outdated
…ter.failed

formatter.failed is sticky across siblings on can_throw_stack_overflow=false
sinks, so a deep branch would suppress every following sibling's .errors
expansion. has_exception() alone covers the can_throw=true RangeError case;
the post-for_each !failed guard still prevents clearing a propagating
RangeError.
Comment thread src/jsc/VirtualMachine.rs
…tually threw

Narrow the post-for_each clear guard to !(failed && can_throw_stack_overflow)
so a hostile .errors iterator after a deep sibling is still cleared on the
uncaught-throw / unhandled-reject sinks, while the RangeError on the
Bun.inspect / console.* sinks keeps propagating.

@claude claude 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.

Nothing further from me — the three earlier inline threads are all addressed at 055856f. Deferring to a human because this reorders the AggregateError branch to print the AggregateError: <msg> header on every uncaught/rejected AggregateError (fixing #21528 as a side effect of the crash guard), which is a user-visible output-format change; and because it supersedes ~7 open sibling PRs that a maintainer will want to close alongside it.

What was reviewed:

  • Visited-map insert/remove in the aggregate branch mirrors the existing cause-chain idiom at L6142-6167, and both paths share formatter.map so the mixed-agg-cause cycle is caught.
  • errors.is_object() correctly rejects the GetterSetter cell that getDirect returns for accessor .errors, so the throwing-getter shape never enters for_each.
  • The final !(formatter.failed && formatter.can_throw_stack_overflow) guard: on can_throw=true sinks the RangeError propagates; on can_throw=false sinks a hostile-iterator sibling after a deep branch is now cleared.
  • Grepped for other snapshot tests asserting the old header-less AggregateError output; only jsx-template-string-crash.test.ts needed updating.
Extended reasoning...

Overview

The PR guards the AggregateError .errors recursion in VirtualMachine::print_errorlike_object against three segfault vectors — self/mutual cycles (via the existing formatter.map visited pool), deep chains (via formatter.stack_check.is_safe_to_recurse()), and tampered .errors (deleted / accessor / non-iterable, via is_object() gate + post-for_each clear_exception). It also seats StackCheck::init() on the two run_error_handler formatter sites so the pre-existing cause-chain depth guard is actually live on the uncaught-throw / unhandled-reject sinks. As a structural consequence of moving print_error_from_maybe_private_data above the .errors iteration, an uncaught AggregateError now prints its own AggregateError: <msg> header before its children — which is the feature request in #21528.

Files touched: src/jsc/VirtualMachine.rs (+67/-26), src/runtime/jsc_hooks.rs (+1), a new 48-cell subprocess test matrix, and a 2-line snapshot update in jsx-template-string-crash.test.ts for the new header.

Security risks

None identified. This is defensive hardening of the error printer against adversarial error-object shapes; the pre-PR behavior was a segfault (DoS) on all of these inputs, so every path is strictly safer. No new user-controlled data reaches an allocation size, path, or FFI boundary.

Level of scrutiny

High. print_errorlike_object sits on every uncaught-exception / unhandled-rejection / console.error / Bun.inspect path. The exception-propagation semantics around for_each are subtle enough that they took three review rounds here (swallowing an intentional RangeError → over-suppressing sibling iteration via sticky formatter.failed → leaking a hostile-iterator exception on non-throwing sinks), each addressed in a follow-up commit. The current shape at 055856f looks correct to me on all four sink families.

Other factors

  • User-visible output change: every multi-error bun -e parse failure and every uncaught AggregateError now gains a leading AggregateError: N errors … line. That is the requested behavior in #21528, but it's a format change a maintainer should sign off on rather than a bot.
  • Duplicate PRs: the find-duplicates bot lists seven open PRs (#34892, #35820, #35174, #31988, #35816, #35826, #35050) each covering a subset of this fix. A human should pick this one (or one of those) as canonical and close the rest.
  • Test coverage is thorough (7 shapes × 6 sinks + depth + output assertions, all describe.concurrent, ~9s under debug+ASAN per the author). The gate evidence shows 38 of the 48 cells crash on unfixed ASAN and all pass with the fix.
  • The comment-cop bot flagged five long comments in VirtualMachine.rs; the author noted (and I confirmed) that they are pre-existing on main and only moved in the diff.

@robobun

robobun commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator Author

This also fixes #36963, which hits the deleted/missing .errors case without any user tampering: when two test files import the same module with a syntax error, JSC's module loader replays the cached fetch error via JSModuleLoader::duplicateError (vendor/WebKit .../runtime/JSModuleLoader.cpp:693), which re-creates the ErrorInstance with the AggregateError errorType but does not copy the errors property. print_errorlike_object then calls for_each on the empty value returned by getDirect and segfaults at address 0x5.

I verified a minimal guard at the same site fixes it, with regression tests for the two-test-file repro and the delete e.errors case, on branch farm/de7c9ee4/aggregate-error-missing-errors (main...farm/de7c9ee4/aggregate-error-missing-errors) in case the tests are useful here. Consider adding "Fixes #36963" to this PR.

@robobun

robobun commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator Author

Closing in favor of #36602, where this has been folded in: the stack check is seated for the uncaught-exception and unhandled-rejection reporters (both now build their formatter in VirtualMachine::print_exception; Formatter::stack_check became pub(crate) in #36184, so the jsc_hooks.rs change here no longer compiles as written), the .errors walk only runs when the property is still an array, and the visited set covers self/mutual cycles and the mixed cause/errors cycle. The test matrix from this PR (cycle, deleted, accessor, non-array, deep aggregate and deep cause chains on the console and uncaught sinks) and a test for the #36963 bun test repro live in test/js/bun/util/inspect-error.test.js there.

@robobun robobun closed this Aug 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

print AggregateError more distinctly

2 participants