console: apply depth cap to Map/Set/Array entries and Error cause chains
Problem
console.logof a nestedMap,Set,Array,MessageEvent, Errorcausechain, orAggregateErrorprints every level. A 1000-deepMapprints 2 MB. Deeper chains throwRangeError: Maximum call stack size exceededout ofconsole.log. A nestedAggregateErrorchain overflows the native stack.- Cause: only
print_object(src/jsc/ConsoleObject.rs) comparesdepthtomax_depth. The other container printers never compare it. Thecauseloop andagg_iter(src/jsc/VirtualMachine.rs) do not track depth.
Fix
- Each container printer returns
[Array ...],[Map ...],[Set ...],[MapIterator ...], or[MessageEvent ...]past the cap, like[Object ...], through oneprint_depth_exceeded_marker. An empty container still prints[],Map {}, orSet {}. - The
causeloop andagg_iterbumpdepthand print[Error ...]past the cap.agg_iteralso checks the native stack guard (stack_check). The error property dump narrowsmax_depth, so it keeps the caller's cap inouter_max_depthfor these walks. Bun.inspect.tablepasses itsdepthoption as the cell start depth, against a fixedmax_depthof 5.TablePrinter::set_start_depthnow clamps it, so adepthabove 5 prints cells like the default.- Verified:
inspect.test.js(13 new cases, 12 fail on the released bun),bun-inspect-table.test.ts(2 new), and the related console suites.
Background
Formatter(ConsoleObject.rs) is the native walker behindconsole.logandBun.inspect.max_depthcomes from--console-depth,Bun.inspect(x, {depth}), or the default of 2.print_error_instance_body(VirtualMachine.rs) prints an Error, notFormatter. It dumps the error's properties one level deep and walkscauseandAggregateError.errorsitself.{depth: Infinity}setsmax_depthtou16::MAX, anddepthsaturates there. A depth comparison alone cannot stop that walk.
Notes
Sizes before and after, on this branch:
| input | before | after | node |
|---|---|---|---|
100-deep cause chain | ~20 KB | 711 B | 575 B |
1000-deep Map | ~2 MB | 167 B | 60 B |
1000-deep Set | ~2 MB | 152 B | 40 B |
100-deep Array | ~20 KB | 129 B | 20 B |
1000-deep MessageEvent | ~3 MB | 261 B | n/a |
| 3000 causes, 12000 Maps, 20000 AggregateErrors | RangeError or SIGSEGV | truncates | truncates |
Tables. A container nested inside a cell now prints as a marker, like a nested plain object already did: { x: [Object ...] }, [ [Array ...] ], Map(1) { 1: [Map ...] }. Node's console.table prints the same shape ([ [Array] ], Map(1) { 1 => [Map] }). For a depth option above 5, the released bun starts every cell past the cap: a plain object cell prints [Object ...] and the new gates would have done the same to Array, Map, and Set cells. The clamp makes such a depth print cells like the default, object cells included. { depth: 0 } keeps its meaning (console-table.test.ts uses it to mirror console.table). The option is not redefined as a max_depth: #34241 did that and was closed without a merge.
AggregateError at the cap. When the members are one level past the cap, the AggregateError prints like any other error first (name, message, stack), then one [Error ...] per member. Without that, Bun.inspect(agg, { depth: 0 }) printed only the markers. Node prints the same shape: the header, then [errors]: [Array]. The normal output is unchanged: members only, as #39633 left it.
Errors inside Error properties. With err.details = { inner }, err.list = [inner], or an AggregateError inside a property, the cause and the members of the nested error follow the caller's depth: depth: Infinity prints them all, depth: 2 prints [Error ...]. Plain object properties keep the one-level cap.
Cause depth per entry point, measured with a 50-deep chain. console.log: Node prints root + 2 causes, this branch prints root + 2 causes. Uncaught throw: Node prints root + 5 causes, this branch prints root + 8 (the error handler's Formatter::new depth), released bun prints all 50. #39637 changes the error handler's formatter to the console depth. Combined with this PR as written, an uncaught error would print root + 2 causes, and #39637's three-cause test would fail. The cause loop reads the depth cap on purpose so {depth} and --console-depth control it. Whichever PR lands second has to pick: the console depth for causes, or #39637 applying its depth only on the non-Error branch.
test/js/web/console/console-log.expected.txt changes for [[[[Array(1000).fill(4)]]]], which now stops at the fourth level like Node. The removed text (a long array wrapped at indent 10, then ... 900 more items) is asserted again by long arrays get cutoff at a nested indent in console-log.test.ts, through Bun.inspect(x, { depth: 5 }).
stack_check is the formatter's native stack guard. Container printers reach it in print_as_prelude. The error walks do not, so agg_iter calls it directly.
Suites run on the merged branch: inspect.test.js, inspect-error.test.js, reportError.test.ts, console-log.test.ts, console-depth.test.ts, bun-inspect.test.ts, bun-inspect-table.test.ts, console-table.test.ts, build-error.test.ts (244 pass, 1 skip that is also skipped on main).
JSX and Proxy have the same unbounded recursion. #29709 is open with that fix, so it is left out here.
Review history: the AggregateError gate, its stack_check, the print_event gate, the relative max_depth, the state restore before ? in the cause loop, the empty-container order, outer_max_depth, the table clamp, and the AggregateError header at the cap each came from review and each has a test.
The branch merges main as of 2026-09-16. The only textual conflict was in inspect.test.js, where both sides appended a describe block.
no test proof · iteration 3 · platform-specific test(s) that do not run on this machine, deferring to CI, which covers all platforms: test/js/bun/util/inspect.test.js