console: print [Circular] for %j on self-referencing objects instead of throwing

Closed
RO
2mo ago
+79-5

Repro

const c = { a: 1 }; c.self = c;
try { console.log("%j", c); } catch (e) { console.log("THREW:" + e.constructor.name); }
// node:  [Circular]
// bun:   THREW:TypeError

TypeError: JSON.stringify cannot serialize cyclic structures. propagates out of console.log, so a %j in any log format string turns a self-referencing object (ORM rows, req/res pairs, DOM-ish trees) into an exception from the logging call itself.

Cause

PercentTag::J in src/jsc/ConsoleObject.rs called json_stringify_fast and propagated any throw with ?. Node's util.format (and Bun's own ported util.format) wrap the stringify in try/catch and map the circular-structure TypeError to the literal [Circular], rethrowing everything else (throwing toJSON, BigInt).

Fix

Catch the stringify exception in the %j arm, unwrap the JSC::Exception cell, and compare the error's message against JSC's fixed "JSON.stringify cannot serialize cyclic structures." string. On a match print [Circular]; otherwise rethrow the original value unchanged.

Verification

$ bun bd -e 'const c={a:1}; c.self=c; console.log("pre %j post", c)'
pre [Circular] post

Controls still throw (matching Node): {toJSON(){throw new Error}}, 10n, {toJSON(){throw "str"}}.


[review] gate passed · iteration 0 · 2 files touched

fails on main (without fix)
ASAN without fix: 1 FAILED
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/js/web/console/console-log.test.ts
info: syncing channel updates for nightly-2026-05-06-x86_64-unknown-linux-gnu
info: latest update on 2026-05-06 for version 1.97.0-nightly (e95e73209 2026-05-05)
info: component rust-src is up to date
info: checking for self-update (current version: 1.29.0)
bun test v1.4.0 (a542d29c8)

test/js/web/console/console-log.test.ts:
(pass) should log to console correctly [1739.01ms]
(pass) long arrays get cutoff [10.98ms]
(pass) console.group [1715.74ms]
160 |     env: bunEnv,
161 |     stdout: "pipe",
162 |     stderr: "pipe",
163 |   });
164 |   const [stdout, , exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
165 |   expect({ stdout: stdout.replaceAll("\r\n", "\n"), exitCode }).toEqual({
                                                                      ^
error: expect(received).toEqual(expected)

  {
    "exitCode": 0,
    "stdout": 
- "pre [Circular] post
+ "pre THREW:TypeError
  ctl-tojson:Error:tj
  ctl-bigint:TypeError:true
  "
  ,
  }

- Expected  - 1
+ R
... (truncated)

release without fix: 1 FAILED
bun test v1.4.0-canary.1 (1498d7b77)

test/js/web/console/console-log.test.ts:
(pass) should log to console correctly [195.88ms]
(pass) long arrays get cutoff [0.17ms]
(pass) console.group [64.97ms]
160 |     env: bunEnv,
161 |     stdout: "pipe",
162 |     stderr: "pipe",
163 |   });
164 |   const [stdout, , exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
165 |   expect({ stdout: stdout.replaceAll("\r\n", "\n"), exitCode }).toEqual({
                                                                      ^
error: expect(received).toEqual(expected)

  {
    "exitCode": 0,
    "stdout": 
- "pre [Circular] post
+ "pre THREW:TypeError
  ctl-tojson:Error:tj
  ctl-bigint:TypeError:true
  "
  ,
  }

- Expected  - 1
+ Received  + 1

      at <anonymous> (/workspace/bun/test/js/web/console/console-log.test.ts:165:65)
(fail) console.log %j prints [Circular] for self-referencing objects [94.45ms]
(pass) console.log with SharedArrayBuffer [0.09ms]

 4 pass
 1 fail
 2 snapshots, 10 expect() calls
Ran 5 tests across 1 file. [554.00ms]
__F:1:S:0
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/web/console/console-log.test.ts
info: syncing channel updates for nightly-2026-05-06-x86_64-unknown-linux-gnu
info: latest update on 2026-05-06 for version 1.97.0-nightly (e95e73209 2026-05-05)
info: component rust-src is up to date
info: checking for self-update (current version: 1.29.0)
bun test v1.4.0 (a542d29c8)

test/js/web/console/console-log.test.ts:
(pass) should log to console correctly [1451.09ms]
(pass) long arrays get cutoff [22.57ms]
(pass) console.group [720.54ms]
(pass) console.log %j prints [Circular] for self-referencing objects [494.39ms]
(pass) console.log with SharedArrayBuffer [11.57ms]

 5 pass
 0 fail
 2 snapshots, 10 expect() calls
Ran 5 tests across 1 file. [6.81s]
__F:0:S:0

release with fix: all passed
$ bun scripts/build.ts --profile=release
info: syncing channel updates for nightly-2026-05-06-x86_64-unknown-linux-gnu
info: latest update on 2026-05-06 for version 1.97.0-nightly (e95e73209 2026-05-05)
info: component rust-src is up to date
info: checking for self-update (current version: 1.29.0)
[configured] bun-profile → bun (stripped) in 2124ms (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); 244 extern-C blocks audited
[1/6] cargo bun_bin → libbun_rust.a (--target x86_64-unknown-linux-gnu)
info: syncing channel updates for nightly-2026-05-06-x86_64-unknown-linux-gnu
info: latest update on 2026-05-06 for version 1.97.0-nightly (e95e73209 2026-05-05)
info: component rust-src is up to date
info: component rust-std is up to date

info: checking for self-update (current version: 1.29.0)
  nightly-2026-05-06-x86_64-unknown-linux-gnu unchanged - rustc 1.97.0-nightly (e95e73209 2026-05-05)

    Blocking waiting for file lock on build directory
   Compiling bun_core v0.0.0 (/workspace/bun/src/bun_core)
[9
... (truncated)
diff hotspot
src/jsc/ConsoleObject.rs                | 59 ++++++++++++++++++++++++++++++---
 test/js/web/console/console-log.test.ts | 25 ++++++++++++++
 2 files changed, 79 insertions(+), 5 deletions(-)

gate history · 1 passed · 0 rejected · iteration 0

evidence per changed file
file                                     reads  edits  tests
src/jsc/ConsoleObject.rs                     8      6      0
test/js/web/console/console-log.test.ts      4      4      0