Skip to content

Print the AggregateError header before iterating its members - #35174

Closed
robobun wants to merge 4 commits into
mainfrom
farm/dcbf384a/aggregate-error-header
Closed

Print the AggregateError header before iterating its members#35174
robobun wants to merge 4 commits into
mainfrom
farm/dcbf384a/aggregate-error-header

Conversation

@robobun

@robobun robobun commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator

The native error printer (used by console.log/console.error of an Error, an uncaught throw, an unhandled rejection, and Bun.inspect) handled AggregateError by iterating .errors and printing each member, then returning. The aggregate's own AggregateError: <message> header and stack were never printed, so a Promise.any() rejection or any thrown AggregateError showed only the member errors with no top-level context. util.inspect (the Node-compat path) was already correct.

Repro

// bun -e 'throw new AggregateError([new Error("m1"), new RangeError("m2")], "TOP-AGG-MESSAGE")'

Before:

1 | throw new AggregateError([new Error("m1"), new RangeError("m2")], "TOP-AGG-MESSAGE")
                                  ^
error: m1
      at [eval]:1:31
1 | throw new AggregateError([new Error("m1"), new RangeError("m2")], "TOP-AGG-MESSAGE")
                                                   ^
RangeError: m2
      at [eval]:1:48

After:

1 | throw new AggregateError([new Error("m1"), new RangeError("m2")], "TOP-AGG-MESSAGE")
              ^
AggregateError: TOP-AGG-MESSAGE
      at [eval]:1:11

1 | throw new AggregateError([new Error("m1"), new RangeError("m2")], "TOP-AGG-MESSAGE")
                                  ^
error: m1
      at [eval]:1:31

1 | throw new AggregateError([new Error("m1"), new RangeError("m2")], "TOP-AGG-MESSAGE")
                                                   ^
RangeError: m2
      at [eval]:1:48

Cause

VirtualMachine::print_errorlike_object entered the is_aggregate_error branch first, iterated .errors via for_each -> agg_iter -> print_errorlike_object, and returned. The normal print_error_from_maybe_private_data path (which renders name, message, and stack) was never reached for the aggregate itself. The .errors property is DontEnum, so the own-property dump inside print_error_instance_body never sees it either.

Fix

src/jsc/VirtualMachine.rs: move the .errors iteration in print_errorlike_object to run after print_error_from_maybe_private_data, so the aggregate prints its own header and stack first, then each member follows with a blank-line separator (matching the existing cause chain layout).

Tests

New AggregateError suite in test/js/bun/util/inspect-error.test.js covering Bun.inspect, console.error, an uncaught throw, an unhandled rejection, and Promise.any. All five fail on main (no header in the output) and pass with this change. Existing inline snapshots in that file are regenerated for the added import line; normalizeError is widened to also strip the newer at require (N:N) debug-only frame shape so the minified-file snapshots stay stable across debug/release.

The duplicate render of a primitive .errors member (error: x followed by x) is pre-existing behaviour of print_error_instance_body for non-ErrorInstance values (also visible via reportError("x")) and is left unchanged here.

Not addressed here

An AggregateError reached via .cause (e.g. throw new Error("outer", { cause: new AggregateError([...], "agg") })) now prints AggregateError: agg thanks to this change, but its .errors members are still omitted. That path recurses through print_error_instance_js (the errors_to_append loop in print_error_instance_body), which never reaches the .errors iteration in print_errorlike_object. Pre-existing and left for a follow-up, likely by moving the iteration into print_error_instance_body so both entry points share it and reuse the existing circular-reference guard there.

Related: #34892 guards the same branch against deep recursion; the two changes compose (whichever lands second has a trivial rebase).

Fixes #21528


[review] gate passed · iteration 1 · 3 files touched

fails on main (without fix)
ASAN without fix: 7 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.test.js test/regression/issue/jsx-template-string-crash.test.ts
bun test v1.4.0 (88317f782)

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  - 4
+ 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 (573f9462e)

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

test/js/bun/util/inspect-error.test.js:
(pass) error.cause [0.24ms]
(pass) Error [0.12ms]
(pass) BuildMessage [0.37ms]
(pass) Error inside minified file (no color)  [1.51ms]
(pass) Error inside minified file (color)  [0.53ms]
(pass) Inserted originalLine and originalColumn do not appear in node:util.inspect [7.77ms]
(pass) observable properties > sourceURL is observable [0.21ms]
(pass) observable properties > line is observable [0.08ms]
(pass) observable properties > column is observable [0.05ms]
(pass) AggregateError > console.error prints the aggregate header and each member [10.84ms]
(pass) AggregateError > uncaught throw prints the aggregate header and each member [17.17ms]
(pass) AggregateError > unhandled rejection prints the aggregate header and each member [20.86ms]
(pass) AggregateError > Bun.inspect prints the aggregate header and each member [25.11ms]
(pass) AggregateError > unhandled Promise.any reject
... (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.test.js test/regression/issue/jsx-template-string-crash.test.ts
bun test v1.4.0 (88317f782)

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

test/js/bun/util/inspect-error.test.js:
(pass) error.cause [5.63ms]
(pass) Error [3.46ms]
(pass) BuildMessage [23.04ms]
(pass) Error inside minified file (no color)  [133.77ms]
(pass) Error inside minified file (color)  [73.02ms]
(pass) Inserted originalLine and originalColumn do not appear in node:util.inspect [440.14ms]
(pass) observable properties > sourceURL is observable [9.17ms]
(pass) observable properties > line is observable [3.26ms]
(pass) observable properties > column is observable [2.85ms]
(pass) AggregateError > unhandled Promise.any rejection prints the aggregate header [449.89ms]
(pass) AggregateError > uncaught throw prints the aggregate header and each member [602.93ms]
(pass) Ag
... (truncated)

release with fix: all passed
$ bun scripts/build.ts --profile=release
[configured] bun-profile → bun (stripped) in 845ms (unchanged)
ninja: Entering directory `/workspace/bun/build/release'
[1/6] gen generated_host_exports.rs
generated_host_exports.rs: 91 exports (host=3, lazy=10, generic=78, rust=0); 237 extern-C blocks audited
[1/6] cargo bun_bin → libbun_rust.a (--target x86_64-unknown-linux-gnu)

  nightly-2026-07-20-x86_64-unknown-linux-gnu unchanged - rustc 1.99.0-nightly (9f36de775 2026-07-19)

^[[1m^[[92m   Compiling^[[0m bun_core v0.0.0 (/workspace/bun/src/bun_core)
^[[1m^[[92m   Compiling^[[0m bun_errno v0.0.0 (/workspace/bun/src/errno)
^[[1m^[[92m   Compiling^[[0m bun_ptr v0.0.0 (/workspace/bun/src/ptr)
^[[1m^[[92m   Compiling^[[0m bun_boringssl_sys v0.0.0 (/workspace/bun/src/boringssl_sys)
^[[1m^[[92m   Compiling^[[0m bun_safety v0.0.0 (/workspace/bun/src/safety)
^[[1m^[[92m   Compiling^[[0m bun_zlib_sys v0.0.0 (/workspace/bun/src/zlib_sys)
^[[1m^[[92m   Compiling^[[0m bun_cares_sys v0.0.0 (/workspace/bun/src/cares_sys)
^[[1m^[[92m   Compiling^[[0m bun_zstd v0.0.0 (/workspace/bun/src/zstd)
^[[1m^[[92m   Compiling^[[0m bun_picohttp v0.0.0 (/workspace/bun/src/picohttp)
^[[1m^[[92m   Compiling^[[0m bun_output v
... (truncated)
diff hotspot
src/jsc/VirtualMachine.rs                          |  26 ++---
 test/js/bun/util/inspect-error.test.js             | 110 +++++++++++++++------
 .../issue/jsx-template-string-crash.test.ts        |  10 +-
 3 files changed, 100 insertions(+), 46 deletions(-)

gate history · 2 passed · 0 rejected · iteration 1

evidence per changed file
file                                                     reads  edits  tests
src/jsc/VirtualMachine.rs                                    8      1      0
test/js/bun/util/inspect-error.test.js                       3      7      0
test/regression/issue/jsx-template-string-crash.test.ts      1      0      0

The native error printer (console.log/error of an Error, uncaught throw,
unhandled rejection, Bun.inspect) handled AggregateError by iterating
.errors and printing each member, then returning. The aggregate's own
'AggregateError: <message>' line and stack were never printed, so a
Promise.any() rejection or any thrown AggregateError showed only the
member errors with no indication of what actually failed.

Move the aggregate-branch iteration in print_errorlike_object to after
print_error_from_maybe_private_data so the aggregate itself is rendered
first (name, message, stack), then each .errors member follows with a
blank-line separator, matching how the cause chain is already printed.
@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Changes

AggregateError printing now initializes exception data earlier and separates child errors with newlines. Tests update stack normalization, location snapshots, AggregateError subprocess coverage, and JSX diagnostic snapshots.

AggregateError rendering

Layer / File(s) Summary
AggregateError rendering path
src/jsc/VirtualMachine.rs
Exception data is reborrowed before AggregateError handling, callback context uses the mutable exception list, and each child error receives a trailing newline.
Error inspection and AggregateError tests
test/js/bun/util/inspect-error.test.js
Stack normalization and location snapshots are updated, and subprocess tests verify AggregateError headers, member ordering, and unhandled Promise.any output.
JSX diagnostic snapshots
test/regression/issue/jsx-template-string-crash.test.ts
Regression snapshots expect AggregateError wrappers and revised JSX parse-error messages.

Possibly related PRs

  • oven-sh/bun#34497: Modifies AggregateError rendering and child-error output formatting.
  • oven-sh/bun#34892: Changes AggregateError handling in VirtualMachine::print_errorlike_object.
  • oven-sh/bun#35039: Adjusts AggregateError member output and formatting behavior.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The change matches #21528 by printing AggregateError's own message and stack before iterating .errors.
Out of Scope Changes check ✅ Passed The test and snapshot updates support the AggregateError rendering change and don't introduce unrelated scope.
Title check ✅ Passed The title accurately summarizes the main change: printing the AggregateError header before its members.
Description check ✅ Passed The description is detailed and covers purpose plus verification, but it doesn't use the template's exact section headings.

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

@robobun

robobun commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 4:49 PM PT - Jul 22nd, 2026

@robobun, your commit 88317f7 has 2 failures in Build #78084 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 35174

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

bun-35174 --bun

@github-actions

Copy link
Copy Markdown
Contributor

Found 2 issues this PR may fix:

  1. print AggregateError more distinctly #21528 - Directly reports AggregateError's own message/header not being printed, only member errors shown
  2. Unsupported proper logging of AggregateError, Error.cause, modified/accessed Error.stack #1352 - Reports AggregateError not being displayed when logged (among other error logging issues)

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

Fixes #21528
Fixes #1352

🤖 Generated with Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

This PR may be a duplicate of:

  1. Fix: print AggregateError name and message before child errors #35050 - Same fix: prints AggregateError name and message before child errors
  2. Fix: print AggregateError name and message before child errors #34497 - Same fix: prints AggregateError name and message before child errors

🤖 Generated with Claude Code

@robobun

robobun commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator Author

Same bug as #35050 / #34497 (both by the same author, identical diff, CI blocked on fork). This PR takes a different route: instead of reading .name/.message separately and printing only the header line, it moves the existing .errors iteration to after print_error_from_maybe_private_data, so the aggregate is rendered through the same path as any other Error (source preview, name/message, stack, own props). That avoids the observable property reads flagged on #34497 and also prints the aggregate's stack. Tests are included for Bun.inspect, console.error, an uncaught throw, an unhandled rejection, and Promise.any.

The transpiler's multi-error AggregateError now prints its '2 errors
building ...' header before the individual BuildMessages.

@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

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@test/js/bun/util/inspect-error.test.js`:
- Around line 198-201: Strengthen the assertions in
test/js/bun/util/inspect-error.test.js at lines 198-201 to verify header <
“error: m1” < “RangeError: m2” and require blank-line separation at both member
boundaries. At lines 214-216, assert the Promise.any header and message are
present, verify r1 precedes r2, and assert the required blank-line separation;
replace containment-only checks with order- and boundary-sensitive assertions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: b0fa66c1-db60-4ef6-98cd-b5e471022728

📥 Commits

Reviewing files that changed from the base of the PR and between 47597ab and 573f946.

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

Comment thread test/js/bun/util/inspect-error.test.js
@robobun

robobun commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator Author

CI on 88317f7 (build #78084): the remaining failures are unrelated to this change.

Pre-existing on main:

  • test/js/node/test/parallel/test-net-connect-memleak.js (FinalizationRegistry + GC timing around net.Socket); reported for main-break triage.
  • test/js/node/test/parallel/test-gc-http-client-connaborted.js (HTTP client GC timing, timeout).

Flaky (passed on retry elsewhere):

  • test/js/bun/webview/webview-chrome.test.ts (animation click timing)
  • test/js/node/test/parallel/test-fs-promises-file-handle-readFile.js (FileHandle GC-close)
  • test/js/node/test/parallel/test-repl-close.js (Windows EPIPE)

The tests touched by this PR (inspect-error.test.js, jsx-template-string-crash.test.ts) are green on every lane. Ready for a look.

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

Beyond the two inline nits, I also checked: the early return in the aggregate branch now skips the was_internal tail after computing it — safe, since an AggregateError is a JS ErrorInstance and print_error_from_maybe_private_data returns false for it; the exception_list.as_deref_mut() reborrow still threads the same list into AggCtx for member errors; and the widened normalizeError regex only strips path-less at name (N:N) frames, so real [dir]/file:N:N frames survive.

Extended reasoning...

The core change (moving print_error_from_maybe_private_data above the is_aggregate_error branch) is sound and well-covered by the new subprocess tests. The two inline findings are both non-blocking nits — one pre-existing (.cause recursion path), one cosmetic (double blank between BuildMessage members, now certified by the regenerated snapshot). Leaving the approve/merge call to a human given the snapshot-spacing question and the two open duplicate PRs (#35050 / #34497) that need reconciling.

Comment thread src/jsc/VirtualMachine.rs
Comment thread src/jsc/VirtualMachine.rs
@robobun

robobun commented Aug 1, 2026

Copy link
Copy Markdown
Collaborator Author

Superseded by #36602, which moves the .errors iteration into print_error_instance_body (so an AggregateError reached via .cause also prints its members), adds [cause]:/[errors]: labels, and fixes the V8 stack-string re-parse/remap for reassigned .stack. Covers the remaining #1352 cases this PR left for a follow-up.

@robobun

robobun commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator Author

Closing in favor of #36602, which contains this change (the .errors walk now runs after the aggregate's own header, from print_error_instance_body so cause-chain aggregates are covered too) together with the same test cases (console.error, Bun.inspect, uncaught throw, unhandled rejection, Promise.any) and the cycle/depth/tampered-property guards.

@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