error printer: print the AggregateError header, label [cause]/[errors] blocks, and guard the .errors walk - #36602
error printer: print the AggregateError header, label [cause]/[errors] blocks, and guard the .errors walk#36602robobun wants to merge 11 commits into
Conversation
…tive error output The native error printer used by console.error, Bun.inspect, uncaught throws, and unhandled rejections had four related problems: - print_errorlike_object short-circuited on AggregateError and iterated .errors only, so the AggregateError's own name, message, stack, and cause were never shown. - Appended cause/error-typed properties were printed as bare second blocks with no label, so it was impossible to tell which was the cause and which were AggregateError members. - V8StackTraceIterator::parseFrame returned false on 'at /path:l:c' frames (no function name), terminating iteration. A reassigned Error.stack containing such frames lost everything from that point on. - remap_zig_exception re-applied source-map remapping to frames that were already in original coordinates (remapped = true), so a reassigned .stack string round-tripped to wrong positions. Move the .errors iteration into print_error_instance_body so the aggregate prints its own header first and its members follow; track the origin of each appended error and emit a dim '[cause]:' / '[errors]:' / '[<prop>]:' label before each block; let the V8 stack parser treat a paren-less line as 'sourceURL:line:col' with an empty function name; and skip the secondary remap for frames already marked remapped. The 'getError frame missing after reading .stack' case in the issue is JavaScriptCore tail-call elimination, not a printer bug. Fixes #1352
|
Warning Review limit reached
Next review available in: 5 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the 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 configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (5)
WalkthroughChangesError formatting
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
Comment |
|
Found 6 issues this PR may fix:
🤖 Generated with Claude Code |
|
This PR may be a duplicate of:
🤖 Generated with Claude Code |
|
Updated 5:44 AM PT - Aug 13th, 2026
✅ @robobun, your commit d77ccd9230b8f1dc12262c52567e2cd1a984bb21 passed in 🧪 To try this PR locally: bunx bun-pr 36602That installs a local version of the PR into your bun-36602 --bun |
…xit-code ordering - widen normalizeError to strip 'at require (51:24)' shaped builtin frames (debug-build coordinates that differ from release), and regenerate the minified-file snapshots without them - V8StackTraceIterator: strip a leading 'async ' and set isAsync in the paren-less branch so 'at async /path:l:c' does not bake the prefix into sourceURL - clear pending exceptions from get_length/get_index in the .errors iteration on both allow_side_effects branches, matching the old for_each behavior this replaces - assert exit code after stderr content in the #1352 test helper and add an 'at async /path:l:c' frame to the reassigned-stack test
There was a problem hiding this comment.
I didn't find new issues — all four earlier findings (normalizeError snapshot leak, async prefix on paren-less frames, exit-code assertion ordering, exception discipline on get_index/get_length, plus the 100-member cap) are addressed. Deferring because this changes the user-visible native error format ([cause]: / repeated [errors]: labels, AggregateError header on build errors) and reworks the core error-printer recursion path; a maintainer should sign off on the output shape and the overlap with #36580/#35174.
What was reviewed:
errors_to_appendlabel lifecycle —dupe_ref/static_paired withderef()inUnprotectAll::drop, andprotect/unprotectbalanced on every push.- Recursion switch from
print_error_instance_js?toprint_errorlike_object— cycle guard viaformatter.mapstill applies; the stack-safety guard onprint_error_instance_jsremains in the chain. remap_zig_exceptionskip onframes[i].remapped— matches the existingframes[top]handling; the.stack-parse path is the only setter.V8StackTraceIteratorparen-less branch —hasParenscomputed beforelineis mutated forasync, soopeningParenthesesindices stay valid.
Extended reasoning...
Overview
Four coordinated fixes to the native error printer: (1) removes the ~70-line is_aggregate_error short-circuit in print_errorlike_object and re-implements .errors iteration inside print_error_instance_body so the aggregate's own header/stack print first; (2) threads a label bun_core::String through errors_to_append and emits dim [cause]: / [errors]: markers; (3) makes V8StackTraceIterator::parseFrame accept at /path:l:c and at async /path:l:c frames instead of terminating; (4) skips secondary source-map remapping in remap_zig_exception for frames already marked remapped. Tests: an 8-case subprocess suite in inspect-error.test.js, three regenerated inline snapshots (with a widened normalizeError filter for internal-builtin frames), and two updated build-error snapshots in jsx-template-string-crash.test.ts.
Security risks
None. No untrusted external input reaches the changed code — the printer operates on the user's own thrown/logged values. The .errors iteration is now capped at 100, and get_length/get_index failures unconditionally clear_exception() (matching the removed for_each semantics). No auth/crypto/network surface.
Level of scrutiny
High. print_errorlike_object / print_error_instance_body run on every uncaught throw, unhandled rejection, console.error(Error), and Bun.inspect(Error). The change is behavior-visible to every user (new labels, AggregateError headers now appear on build-error output), reworks a JSC-interop path with protect/unprotect and string-refcount pairing, and changes source-map remapping and stack-string parsing that other issues (#15859, #32390, #22339) depend on. It also supersedes/overlaps two open PRs. The output-format choice — one [errors]: label per member rather than one for the group, and [cause]: rather than Node's braced style — is a design call a maintainer should confirm.
Other factors
All four of my earlier inline findings are resolved in 7cc8524 and 1f67f05: normalizeError now filters (\d+:\d+) frames so the minified-file snapshots don't pin debug-bundled builtin coordinates; the paren-less parseFrame branch strips async and sets isAsync; run() returns exitCode and callers assert it after content; get_index/get_length errors now clear_exception() unconditionally; and .errors printing is capped at 100. The comment-cop threads are resolved. Test coverage is solid (variant matrix across console.error/Bun.inspect/throw/reject/Promise.any, cause-chain-to-aggregate, reassigned V8 stack), and the gate evidence shows all 11 new/updated tests fail on main and pass on both debug-ASAN and release. Given the breadth of user-visible output change and the core-path location, this warrants a human sign-off rather than auto-approval.
|
Overlaps with #36726 (opened for #15859 before I saw this one). Same If useful, #36726's branch has a dedicated regression test at |
|
Data point for fix 4 (the class Foo { constructor() { throw new Error("x"); } }
function g() {
return new Foo();
}
try { g(); } catch (e) {
const stack = e.stack; // materializes .stack, JSC drops the frame vector
const printed = Bun.inspect(e); // re-parses the string, every frame arrives remapped=true
console.log(stack.split("\n")[2]);
console.log(printed.split("\n").find(l => l.includes("at g ")));
}The The tests in this PR do not appear to exercise that hunk directly: the reassigned |
…error-printer-1352
…p-level error visited, count omitted AggregateError members Both uncaught-exception sinks now build their formatter in VirtualMachine::print_exception, which initializes the stack check that print_error_instance_js consults, so a deep cause or .errors chain stops printing instead of overflowing the native stack. That check now reserves 256 KB of extra headroom on non-Windows platforms too: with the 128 KB default, a debug+ASAN build still overflowed in about one run in four because one print cycle (transpiler source lookup plus allocator slow paths) was measured using ~130 KB below a passing check. The error being printed is registered in the visited set while its cause/.errors members print, so a chain leading back to it renders as [Circular] right away, and the member loop stops once the formatter has failed. Members past the 100 member cap are reported as "... N more errors". Tests cover the cycle, tampered-property, depth and cap cases on the console and uncaught sinks, the exact uncaught layout, and the bun test crash from a module that fails to build for a second test file.
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 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 `@src/jsc/bindings/ZigException.cpp`:
- Around line 299-301: Update delimiter validation in the frame-location parsing
logic around hasParens so the location-only path is selected only when both
openingParentheses and closingParentheses are absent. Treat exactly one
delimiter as malformed and preserve the existing failure return instead of
parsing it as a location; add regressions covering unmatched opening and
unmatched closing delimiters.
In `@src/jsc/VirtualMachine.rs`:
- Around line 6412-6416: Update the visited-map lookups in the relevant
VirtualMachine code, including the expression assigning registered_self, to
handle ArrayHashMap::get_or_put allocation errors through
bun_core::handle_oom(...) or unwrap_or_oom() instead of expect("unreachable").
Apply the same replacement to both occurrences and preserve the existing
found_existing behavior.
In `@test/js/bun/util/inspect-error.test.js`:
- Around line 260-275: Add a regression test alongside the existing
reassigned-stack test that creates a source-mapped file, materializes the error
stack, and compares a non-top frame’s location from Bun.inspect(e) with the
corresponding frame in e.stack. Assert both report the same original source
location, covering the frames[i].remapped guard in VirtualMachine and the `#15859`
double-remap scenario.
- Around line 478-494: Strengthen both uncaught-throw and unhandled-rejection
parameterized tests by asserting that the rendered deep error output is bounded,
not merely that the outer header exists. Add an upper-bound assertion on the
number of “error: level” headers or on stderr.length, while preserving the
existing header and exitCode assertions.
🪄 Autofix
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: 269bf82f-c4d9-4bb3-96c9-6ce9368bef78
📒 Files selected for processing (5)
src/jsc/VirtualMachine.rssrc/jsc/bindings/ZigException.cppsrc/runtime/jsc_hooks.rstest/js/bun/util/inspect-error.test.jstest/regression/issue/jsx-template-string-crash.test.ts
…ed TypeScript fixture)
|
Status: this PR is the consolidated fix for the AggregateError printing cluster. Closed in its favor: #35174, #36580 (@TheRodzz, same root cause), #35825, #31988, #34892, #35820. Reproduced on main ( Self-review of the consolidated diff turned up three problems, all fixed in the follow-up commits: non-Error Remaining CI failures on the last build were retried-green or on unrelated lanes ( Related, left open on purpose: #35816 (its VirtualMachine.rs hunk conflicts with this PR, the Event parts are independent), #35532 (Promise.any's empty message). |
There was a problem hiding this comment.
Beyond the inline finding: the two formatter.map.get_or_put(...).expect("unreachable") calls (also flagged by CodeRabbit) were examined — they mirror the pre-PR sibling at the same site and the map is bounded by the 100-member cap plus the stack-check depth guard, so the OOM path is not reachable in practice. Commit 50d5699 added the source-mapped TypeScript fixture that covers the frames[i].remapped guard I flagged earlier; that thread is resolved from my side.
Extended reasoning...
The bug-hunting pass surfaced one new regression (non-Error object members of .errors now print as [Circular] because the outer loop pre-registers them in formatter.map before formatter.format runs its own cycle check on the same value). That is posted inline and warrants a fix before merge, so I am not approving.
Two concerns raised by other bots were re-examined and ruled out this run: the .expect("unreachable") on get_or_put matches the existing convention at the same call site pre-PR and cannot realistically fail (the map holds at most ~100 entries per level, bounded by MAX_AGGREGATE_ERRORS_PRINTED and the stack check); and the deep-chain uncaught-throw tests are crash guards, so asserting only the header plus a clean exit is the intended invariant — a truncation-count assertion would couple the test to the platform-dependent stack-check depth.
Since my last review the author added the #15859 source-mapped-TypeScript test in 50d5699, which directly exercises the || frames[i].remapped clause (positions in stderr must equal positions in error.stack). My earlier coverage nit on that clause is addressed. The undrained stdout: "pipe" at the two inline Bun.spawn sites remains as previously noted.
…lanced stack frames; review follow-ups
A plain-object member of .errors was pre-registered in the formatter's
visited map before being handed to formatter.format, which then reported
it as [Circular]. Register only ErrorInstance members; the formatter
tracks everything else itself.
V8StackTraceIterator: a frame with exactly one parenthesis (or ')' before
'(') stops parsing as it did before; only a frame with neither is parsed
as a bare location.
Use handle_oom for the visited-map inserts, trim comments, drain stdout in
the two remaining spawns, and make the deep-chain fixtures deep enough to
exhaust the stack on release builds too (a Windows release build printed
all 3000 levels), asserting that fewer levels than the chain has were
printed.
There was a problem hiding this comment.
I reviewed the latest revision and didn't find any new bugs — the earlier findings (plain-object .errors members printing as [Circular], missing #15859 coverage, undrained stdout pipes, unbalanced-paren frames) are all addressed. Given the scope — reworked print_errorlike_object recursion, a changed print_exception signature, an empirically-tuned 256 KB stack headroom bump, new clear_exception() calls in the .errors walk, and a user-visible output-format change — a human look is still worthwhile.
What was reviewed:
- Cycle-map registration now gated on
JSType::ErrorInstance, so plain-object members reachformatter.formatuntracked and print correctly; the back-reference case still short-circuits via theerror_instanceself-registration. V8StackTraceIterator::parseFrame— unbalanced-paren frames now returnfalsebefore the location-only path;asyncprefix is stripped for paren-less frames.errors_omittedaccounting whenget_indexthrows mid-walk (len - appended), andUnprotectAlldrop guard now also derefs the label String.
Extended reasoning...
Overview
The PR reworks VirtualMachine::print_errorlike_object / print_error_instance_body in src/jsc/VirtualMachine.rs (~130 lines net), removing the C-ABI for_each special case for AggregateError and folding .errors iteration into the existing errors_to_append list with a label per entry. It changes the signature of print_exception (now takes value: JSValue, exception: Option<&Exception>) and seats StackCheck::init() there so the uncaught/unhandled sinks get the same depth guard as console.*/Bun.inspect. It bumps the non-Windows stack headroom from 0 to 256 KB. In src/jsc/bindings/ZigException.cpp it teaches V8StackTraceIterator::parseFrame to accept at /path:l:c frames with no parentheses and to reject frames with exactly one unmatched paren. src/runtime/jsc_hooks.rs collapses both branches of print_exception to route through the VM helper. ~430 lines of new tests in inspect-error.test.js plus two updated snapshots in jsx-template-string-crash.test.ts.
Security risks
None identified. The change is confined to error-formatting/diagnostic output. The .errors read uses getDirect (no getter invocation), the array walk is capped at 100, and element reads that throw are caught and clear the exception rather than propagating. No auth, crypto, or filesystem paths are touched.
Level of scrutiny
High. This is the native uncaught-exception / unhandled-rejection reporter — a crash here is a crash on every user program that throws. The PR changes recursion structure, adds a magic-number stack-headroom constant derived from gdb measurements under debug+ASAN, introduces two clear_exception() calls (REVIEW.md flags clearException() generally, though the error-printer path is a defensible exception since it already sits inside error handling), and changes user-visible output format ([cause]:/[errors]: labels now appear in every AggregateError render, including build-error output). The consolidated fix supersedes six other PRs, so a maintainer should confirm this is the shape they want to land.
Other factors
- All prior inline findings from this reviewer and CodeRabbit are marked resolved; the current diff reflects those fixes (Error-only cycle-map gating,
handle_oomonget_or_put, unbalanced-paren rejection, source-mapped #15859 test, bounded-output assertions on the deep-chain tests, 3-way pipe drains). - The comment-cop bot has ~10 unresolved "paragraph-long comment" flags on the Rust file; those are style-level and don't affect correctness.
- The last CI status shown (build #93949 for 50d5699) reported
inspect-error.test.jsfailing on Windows x64 and aarch64; a follow-up commit (74323e7) landed after that, so the current Windows status is unknown to me. - Test coverage is thorough (9 tampering shapes × 2 sinks, deep-chain × 3 sinks with depth branched on
isDebug || isASAN, exact-layout snapshot for #21528, #36963 two-file repro, #15859 source-mapped fixture). The candidate concern about the #36963 test hardcoding "4 errors" was ruled out — that count is a stable property of the fixedlib.tsfixture, not a fragile parser-recovery detail.
…tted-members trailer print_error_instance_js handed the caller's exception list to remap_zig_exception and gave the body None, so the cause chain and (since the .errors walk moved here) the AggregateError members never reached the list behind Bun.serve's development error page, and BuildMessage members of a multi-error module no longer reached its build log. Reborrow the list for the remap and pass it on to the body: the page now lists the thrown error, then its cause and members, and the build log is populated as before.
There was a problem hiding this comment.
I reviewed the latest revision (d77ccd9) and the bug-hunting pass found nothing new — both prior findings (the exception_list not reaching nested members, and the ... 1 more errors plural) are addressed and covered by tests. Given the scope — ~200 lines in the native error printer on the uncaught-exception path, a user-visible output format change, an empirically-tuned stack-headroom constant, and consolidation of six other PRs — a maintainer sign-off is still warranted.
What was reviewed:
.errorswalk hardening (getDirect + array check, cycle guard, 100-member cap, throwing-index break) and the ErrorInstance-only visited-map registration — plain-object members verified to reachformatter.formatuncached.exception_listreborrow inprint_error_instance_jsso nested cause/errors reach the dev-error page; new serve.test.ts cases cover both shapes.V8StackTraceIterator::parseFrameparen-less branch — unbalanced/mis-ordered parentheses still stop parsing; the|| frames[i].remappedclause now has a source-mapped fixture that fails without it.print_exceptionsignature change has no other callers;UnprotectAlldrop guard derefs the new labelStringalongsideunprotect().
Extended reasoning...
Overview
The PR reworks how the native error printer handles AggregateError: it removes the early-return special case in print_errorlike_object, folds .errors members into the existing errors_to_append list (alongside cause and error-valued own properties), labels each appended block [cause]: / [errors]: / [<prop>]:, and hardens the walk (getDirect + is_array guard, cycle detection via the formatter's visited map, 100-member cap with a counted trailer, break on throwing index reads, formatter.failed short-circuit). It also seats StackCheck::init() on the uncaught-exception/unhandled-rejection formatter and raises the non-Windows headroom to 256 KB, teaches V8StackTraceIterator::parseFrame to accept paren-less at /path:l:c frames, and skips already-remapped frames in remap_zig_exception. Six files touched; ~430 lines of new tests across inspect-error.test.js, serve.test.ts, and two updated snapshots.
Security risks
None identified. The change reads user-controlled error properties but does so through getDirect (own data slot, no getter invocation) for .errors and clears exceptions on the get_index/get_length paths. No new allocation sizes are derived from untrusted input beyond the existing errors_to_append Vec, which is capped at ≤100+few entries per level and freed by the UnprotectAll drop guard.
Level of scrutiny
High. This is the native error printer on the uncaught-exception / unhandled-rejection path — a bug here either crashes the process while it's already reporting a failure or silently drops diagnostics. It touches JSValue protect/unprotect pairing, the formatter's cycle map, bun_core::String refcounts, and an empirically-derived stack-headroom constant. It also changes user-visible output format (the [cause]:/[errors]: labels and the AggregateError header now appearing on bun -e build failures), which is a UX/API-surface decision a maintainer should confirm. The PR consolidates six other open PRs, so merging it is also a triage decision.
Other factors
The PR has been through several review rounds; every prior finding I raised (test coverage for the remapped clause, undrained stdout pipes, the plain-object [Circular] regression, the exception_list regression for the dev error page, the plural trailer) was addressed with a targeted fix and a test that fails without it. Test coverage is thorough (all four sinks × 9 tampering shapes × 2 sinks, deep-chain bounds on debug/ASAN and release, the #36963 and #15859 repros, the dev-error-page shapes). The robobun evidence gate shows 34+ tests fail on main and pass here across both ASAN and release. No outstanding unresolved threads. Still, the combination of native memory-safety surface, output-format change, and multi-PR consolidation puts this outside what I'd auto-approve.
Consolidates the open AggregateError printing PRs into one change: the header/ordering fix (#35174, #36580 by @TheRodzz, which independently found the same early return), and the
.errorswalk hardening (#35825, which superseded #31988, #34892 and #35820). Those are closed in favor of this PR.Problem
console.error(agg),Bun.inspect(agg), an uncaughtthrow aggand an unhandled rejection of anAggregateErrorprinted only the member errors. The aggregate's ownAggregateError: <message>line, its stack and itscausewere dropped, so aPromise.any()failure or a multi-error module build showed a list of unrelated-looking errors (print AggregateError more distinctly #21528, Unsupported proper logging of AggregateError, Error.cause, modified/accessed Error.stack #1352).VirtualMachine::print_errorlike_objectspecial-casedis_aggregate_error, iterated.errorswithfor_eachand returned before the normal printing path ran (src/jsc/VirtualMachine.rs, the removed block at the top of that function)..errorscurrently holds, so each of these killed the process on every sink (console.log,Bun.inspect, uncaught throw, unhandled rejection):.errorscontaining the aggregate itself (SIGSEGV), two aggregates containing each other (SIGSEGV), a 3000-deep.errorschain (SIGSEGV),delete agg.errors(nullJSCellcall,panic: Segmentation fault at address 0x5in release),.errorsredefined as an accessor (ASSERTION FAILED: isSymbol()in debug, bogusTypeErrorin release).delete agg.errorshappens without user code: when a second test file imports a module that failed to build, JSC's module loader replays the cached failure viaJSModuleLoader::duplicateError, which creates an AggregateError-typed instance with noerrorsproperty, sobun testcrashed at the second file (bun testrunner crash #36963, reproduces on 1.4.0).causechain crashed the uncaught-exception and unhandled-rejection reporters too:print_error_instance_jshas a stack check, but the formatter built for those two sinks never initialized it (Formatter::newleavesstack_checkin its always-passes state).causeblocks were printed with no label, so a chain read as separate errors (improves Bun prints multi-levelError.causechain as separate errors instead of preserving Node-style nested stack trace #32343).V8StackTraceIterator::parseFramestopped at the firstat /path:line:colframe (no function name, no parentheses), which Bun itself emits, so a reassignederr.stackwas truncated; andremap_zig_exceptionre-applied the source map to frames that the.stackparse had already markedremapped, shifting their line numbers (Incorrect line numbers in error stack for TypeScript code #15859).Fix
print_errorlike_objectno longer special-cases AggregateError.print_error_instance_bodyappends the.errorsmembers to the sameerrors_to_appendlist thecausechain already uses, so the aggregate prints its own source preview, header and stack first, and an AggregateError reached through acausealso shows its members..errorsis read withgetDirectand only walked when it is still an array (delete, accessor and reassigned cases print just the header), element reads that throw stop the walk, at most 100 members are appended and the rest are reported as... N more errors.[cause]:/[errors]:/[<property>]:label. The error being printed is registered in the formatter's visited set while its members print, so any chain leading back to it prints[Circular]once instead of printing the error again. Only Error-valued members are tracked this way: a plain-object member (whatPromise.anyproduces when the inputs reject with non-errors) is handed to the regular formatter, which tracks it in the same set itself, so pre-registering it would have printed[Circular]in place of the object. The loop stops as soon as the formatter reports failure (stack check tripped or writer failed).Bun.serve's development error page collects is now passed on to the nested errors as well (print_error_instance_jsused to hand it toremap_zig_exceptionand give the bodyNone), so the page lists the thrown error, then its cause and members, in print order; on main it listed only the members of a thrown AggregateError and nothing for a cause chain. BuildMessage members of a multi-error module reach the page's build log through the same list, as they did on main.VirtualMachine::print_exception, which is the one place that builds their formatter and seatsStackCheck::init(), so the existing depth guard is live for them. Nested members are printed throughprint_errorlike_object, which routes every level through that guard.parseFrameaccepts frames with no parentheses at all (empty function name, optionalasyncprefix); a frame with exactly one parenthesis, or)before(, still stops parsing as before.remap_zig_exceptionskips frames already markedremapped.util.inspectalready does for these errors in Bun and Node (header first,[cause]and[errors]labeled,[Circular]on cycles) while keeping the native printer's layout, and it reuses the guards thecausechain already had instead of adding a second set to a separate walk.test/js/bun/util/inspect-error.test.js(header/labels on all four sinks and via a cause chain, exact uncaught layout snapshot for the print AggregateError more distinctly #21528 program, 9 cycle/tampering shapes x {console.error, uncaught throw} (including plain-object members, with and without a reference back to the aggregate), member cap with 101 and 103 members (... 1 more error/... 3 more errors), deep.errorsandcausechains on console/throw/reject that assert fewer levels than the chain has were printed (3000 levels under debug/ASAN, 50000 on release builds: a Windows release build printed all 3000, and main's release build on Linux segfaults after about 1450 levels of an uncaughtcausechain), the bun testrunner crash #36963 two-test-file repro, reassigned V8-format stack ending in a frame with an unbalanced parenthesis, and a source-mapped TypeScript fixture whose printed frames must equalerror.stackafter it was read, which is the Incorrect line numbers in error stack for TypeScript code #15859 case). The existing development-error-page test intest/js/bun/http/serve.test.tsgained a module with 4 build errors (one AggregateError exception, 4 build-log entries) and a thrown AggregateError with a cause (exceptions listed as aggregate, cause, member, member); it fails on main too. 37 of the 46 tests in the inspect-error file plustest/regression/issue/jsx-template-string-crash.test.tsfail on main'ssrc/(crash or missing header), all pass with this branch; the two build-error snapshots in the jsx test now start with theAggregateError: 2 errors building ...header.bun-inspect.test.ts,inspect.test.js,reportError.test.ts,console-log.test.ts,console-recursive.test.ts(pass);inspect-error-leak.test.jsRSS delta is the same with and without the change (349-357 MB vs 350 MB under ASAN; the test itself only times out in this container);cargo clippyonbun_jsc/bun_runtimeandcargo fmt --checkare clean.Example,
console.error(new AggregateError([new Error("Error 1")], "Aggregate error message.", { cause: new Error("Cause") })):Background
src/jsc/VirtualMachine.rsthat renders an error as source preview +name: message+ own properties + stack. It is used byconsole.*andBun.inspect(throughConsoleObject's formatter), and by the uncaught-exception / unhandled-rejection reporters (throughjsc_hooks::print_exception).util.inspectis a separate JS implementation and was already correct.errors_to_append: the list of error objectsprint_error_instance_bodyprints as separate blocks after the main one. Before this PR it held error-valued own properties and the non-enumerablecause;.errorsis also non-enumerable (DontEnum), which is why it needs explicit handling.formatter.map: the console formatter's visited set, keyed by JS object, used to print[Circular]. Entries are added before recursing into an object and removed afterwards, so it tracks the current path, not everything printed.StackCheck: compares the current stack pointer against the thread's stack bound;is_safe_to_recurse_with_extra(n)requires a platform threshold (128 KB on Linux/macOS, 256 KB on Windows) plusnbytes to remain.Formatter::newdeliberately leaves it uninitialized (always passes);StackCheck::init()arms it. The reserve has to cover everything one print cycle does before the next check, which here includes a transpiler call for the source preview.getDirect: reads an own data property slot without running getters or walking the prototype chain; it returns the empty value when the property is gone and theGetterSettercell when it was redefined as an accessor, so the array check rejects both.JSModuleLoader::duplicateError: when a module's fetch/parse fails, JSC caches the error and, for later importers, throws a fresh copy that keeps the error type but not theerrorsproperty.Superseded description (before the hardening was folded in) and the "missing getError frame" note
The first version of this PR only removed the early return, added the labels and fixed the two stack-string problems; #35825's cycle/depth/tampering guards and tests were folded in afterwards, along with a test for #36963, so that one change can replace #35174, #35825, #36580, #31988, #34892 and #35820.
The #1352 repro's
function getError(msg) { return new Error(msg); }frame is absent fromerr.stackitself, not just from the printed output (wrapping thereturnintry { } finally { }makes it appear in both), so it is a stack capture matter and this PR does not change it.Fixes #21528
Fixes #1352
Fixes #36963
Fixes #15859
[review] gate passed · iteration 0 · 4 files touched
fails on main (without fix)
passes on PR (with fix)
diff hotspot
gate history · 2 passed · 0 rejected · iteration 0
evidence per changed file