Repository navigation
Conversation
…0 to v26.12.1
Bun reports Node.js v26.12.1. Its port of inspect.js still printed these
values as v26.3.0 or an older release did.
- formatNumber: the -0 test runs before the numericSeparator branch and the
number is split at its decimal point. -0 keeps its sign, -0.5 is no longer
"0..5", and exponent forms are left alone.
- formatError: an own `error` or `suppressed` property is printed, so a
SuppressedError shows the two errors it carries.
- getPrefix: a Symbol.toStringTag that ends in the constructor name after a
dot is printed alone ("Intl.NumberFormat {}").
- reduceToSingleString: with breakLength: Infinity and compact: true, an entry
that has a line break ends the line.
- formatExtraProperties: the value is read before the indent, so a detached
DataView does not indent the rest of the output.
- formatProperty: a getter that returns a function is printed as a value. The
indent is restored when the value throws. A getter that throws null or
undefined is reported.
- markNodeModules: the scan of Node.js replaces the RegExp. The name of a
scoped package is underlined whole.
The ansi RegExp of stripVTControlCharacters is not part of this change.
Co-authored-by: 0xfandom <kashyapshivank01@gmail.com>
An own `error` or `suppressed` of an Error now reaches formatValue. These two changes keep util.inspect from throwing for the values Node.js prints there. - #45254: getStackString, removeDuplicateErrorKeys and formatError do not throw when the stack, name, message or errors getter of an error throws. - #46414: formatRaw reads Symbol.toStringTag in a try.
…throws, Float16Array - formatError prints `cause` and `errors` when they are own properties, as it does for `error` and `suppressed` (nodejs/node#61032). An inherited `cause` method or `errors` getter is no longer printed as a key. - formatRaw rethrows an error that is not a stack overflow (nodejs/node#55544). Before, it threw an AssertionError with the text of that error. - Float16Array is in builtInObjects, as in Node.js v26.12.1: showHidden does not walk into its prototype. - The inputs of the new tests are in a fixture that Node.js runs too. One test gives them to a Node.js that is v26.12.1 or later and compares the output.
|
Warning Review limit reachedYou've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Next included review available in 12 minutes. View limit details
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
Updated 11:04 PM PT - Oct 29th, 2026
❌ @robobun, your commit 1211881 has 2 failures in
🧪 To try this PR locally: bunx bun-pr 47303That installs a local version of the PR into your bun-47303 --bun |
|
Status Reproduced on main 11c3671 with this script. The same script exits 0 on a Node.js v26.12.1 binary.
PR: #47303 CI (build 135563, head 1211881, finished)
|
There was a problem hiding this comment.
Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.
Beyond the inline finding, I also checked the observable-behavior shifts this port introduces and ruled them out as deliberate upstream ports rather than regressions: cause/errors switching to an own-property check (nodejs/node#61032), own error/suppressed being printed for any error rather than only SuppressedError (matches upstream's formatError), and the compact: true + breakLength: Infinity line-break change (nodejs/node#64238). Also confirmed the new primordials (StringPrototypeLastIndexOf, StringPrototypeStartsWith, ObjectPrototypeToString) are captured at module load and that isForced and nodeModulesRegExp have no remaining references.
Extended reasoning...
The change ports upstream Node.js inspect.js hunks into src/js/internal/util/inspect.ts (formatNumber, getPrefix, formatError, getStackString, markNodeModules, formatProperty, formatExtraProperties, reduceToSingleString) and adds tests across five test files; it touches no auth, crypto or injection surface, only diagnostic string formatting. The one inline finding is a pre-existing indentation issue in the getter path, and the remaining candidate concerns were traced to intentional upstream behavior, so this note records what else was examined.
|
There is a second set of branches for the same outputs. No PR is open for them, so that this PR stays the only one.
One input prints on main and on Node.js v26.12.1, and const util = require("node:util");
const inner = new Error("x");
inner.stack = "Error: x";
Object.defineProperty(inner, "message", { get() { throw new Error("from the message getter"); } });
const fn = Object.assign(function f() {}, { [util.inspect.custom]() { throw inner; } });
console.log(util.inspect({ get a() { return fn; } }, { getters: true }));
I ran each |
…s not make inspect throw The getter arm of formatProperty sends a function to formatValue since nodejs/node#64839, so what the printing of that function throws reaches the catch, which read err.message with no guard. A thrown error whose message getter throws made util.inspect throw. The catch now reads the message in a try and prints undefined when it cannot. Node.js prints the thrown value there with formatValue. Bun keeps the message: formatError prints a DOMException that Bun throws without its name and message (#37419).
Fixes #23098
Problem
util.inspect()prints several values otherwise than Node.js v26.12.1, which main reports (Build with Bun 1.4.4, update Node.js to 26.12.1 #46533). User report:inspect(-0.12, {numericSeparator: true})is0..12.usingblock prints[SuppressedError]without its errors. A getter that returns a function prints<Inspection threw ...>.src/js/internal/util/inspect.ts, a port of Node'slib/internal/util/inspect.js, is behind upstream.Fix
formatNumber(from fix(node:util): correct numericSeparator for negative fractional numbers #32460, credited in the commit),getPrefix,formatError,formatProperty,formatExtraProperties,markNodeModulesandreduceToSingleString.formatErrorprints ownerror,suppressed,causeanderrors, behind the guards of util.inspect: do not throw when an error's stack, name, message or errors getter throws #45254 and util.inspect: read Symbol.toStringTag in a try, as Node.js does #46414.formatRawrethrows a nested error. The getter catch reads the message in atry.Background
util.format,node:assertandConsoleuseinspect.tstoo.console.logandBun.inspectuse a native formatter.-0.5stays0..5. Considered no guards: 10 of 16 hostile members madeinspectthrow.formatValuein the getter catch: a thrown DOMException of Bun's then prints a raw stack and no message (DOMException (AbortError) prints raw error object and an incorrect stack frame #37419).Downsides
<Inspection threw (...)>Bun still prints the message, and Node.js the thrown value.Map+141 instructions (+3.9%, 1 moreString#lastIndexOf), a fraction withnumericSeparator+646 (+50%), a colored error with frames outside the cwd andnode_modules+8.2%. Module text: +2,412 bytes.Notes
Each change, with the output of main 11c3671 and of this branch. The branch column is also what a Node.js v26.12.1 binary prints, unless the cell says otherwise. "Older" means that Node.js v26.3.0 already had it.
formatNumberinspect(-0, { numericSeparator: true })0-0formatNumberinspect(1.5e-10, { numericSeparator: true })0.5e-_101.5e-10formatNumberinspect(-0.12, { numericSeparator: true })0..12-0.12markNodeModules(/a/node_modules/@s/pkg/i.js:1:1)@sunderlined@s/pkgunderlinedmarkNodeModulesat /a/node_modules/pkgpkgunderlinedreduceToSingleStringinspect({ a: twoLines, b: 1 }, { breakLength: Infinity, compact: true }){ a: l1\n l2, b: 1 }{ a: l1\n l2,\n b: 1 }formatPropertyinspect({ get a() { return function foo() {} } }, { getters: true })[Getter: <Inspection threw (Symbol.prototype.toString requires ...)>], and the rest is indented 2 more{ a: [Getter] [Function: foo] }formatPropertyinspect({ get a() { throw null } }, { getters: true })AssertionError{ a: [Getter: <Inspection threw (null)>] }formatPropertyErrorwhosemessagegetter throwsAssertionError{ a: [Getter: <Inspection threw (undefined)>] }. Node.js prints the error in the parenthesesformatPropertyinspect.customthrows thatError[Getter: <Inspection threw (Symbol.prototype.toString requires ...)>]getPrefixinspect(new Intl.NumberFormat("en"))NumberFormat [Intl.NumberFormat] {}Intl.NumberFormat {}formatErrorinspect(new SuppressedError(1, 2, "m")), no stack frames[SuppressedError: m][SuppressedError: m] { [error]: 1, [suppressed]: 2 }formatErrorcause()method[E [Error]: m] { cause: [Function: cause] }[E [Error]: m]formatError,getStackStringErrorwhosestackgetter throws[Error: m]formatExtraPropertiesinspect([detachedDataView, { a: 1 }])DataViewformatRawinspect({ a: { [inspect.custom]() { throw new RangeError("boom") } } })AssertionError: handleMaxCallStackSize assertion failed: RangeError: boomRangeErrorformatRawSymbol.toStringTaggetter throws{}builtInObjectsinspect(new Float16Array(2), { showHidden: true })breakLength: Infinity: upstream meant util: allow single-line format when break length is infinite nodejs/node#64238 as a fast path with the same output. Its line-break test changes thecompact: truelayout, and this follows that output, bug for bug. Node.js tests each entry. Here the length loop is skipped and the joined text is scanned once.<Inspection threw (...)>Bun prints the message of the thrown value, as main does. It printsundefinedfor a value that has none, and now also for a message that cannot be read or cannot be a string. Node.js prints the thrown value there withformatValue(ctx, err, recurseTimes).err.messagethere with no guard. With functions sent toformatValueinside thetry, the function row of the table madeutil.inspectthrow, where main and Node.js print (report).AbortSignal.abort().throwIfAborted(),structuredClone(() => {})) then prints a raw stack andline,columnandsourceURL, with no name and no message.util.inspectprints such a DOMException that way today: DOMException (AbortError) prints raw error object and an incorrect stack frame #37419, with DOMException: inherit from ErrorInstance for [[ErrorData]] and .stack #32898, DOMException: make line/column/sourceURL on internally created exceptions non-enumerable #39311, DOMException: record source-mapped positions and a Bun-format stack on internally created exceptions #39320 and console: format DOMException as an error, not a plain object #40227 open. A getter that throws anErrorwith a Symbolnamemadeinspectthrow. Main prints the message for both. The switch to Node's line can follow those.{ a: { b: { c: [SuppressedError] } } }. Node.js prints the same.console.logandBun.inspectchange only where aninspect.custommethod calls theinspectthat it gets as its third argument.Not in this PR.
stripVTControlCharacters(util: fix OSC 8 hyperlink stripping in stripVTControlCharacters nodejs/node#64319). It is in node:util: stripVTControlCharacters uses node's current RegExp on the fast replace path #42908, with a guard for a quadratic case of that RegExp on JavaScriptCore.console.log(new SuppressedError(...))still prints no member. That is console,inspect: print SuppressedError .error and .suppressed #36662. Related to Bun doesnt print info about SuppressedError #10336.Proxy(target). That is util.inspect: print proxies as Proxy(...) and unwrap nested proxies like node v26 #38652, and onetest.failinghere names it.<Inspection threw (...)>, and the other parts ofinspect.tsthat are behind upstream. Node's owntest-util-inspect.jsof v26.12.1, run with asserts that record and continue, has 60 failing assert lines on main and 41 on this branch. No assertion that passes on main fails on this branch.%TypedArray%.prototypeis missing fromwellKnownPrototypes, so a typed-array prototype withshowHiddenlists five getters that Node.js does not print. It is older than v26.3.0 and is reported separately.Other open PRs that this touches.
formatRawarm of Format Temporal values in console.log, util.inspect, and test pretty-format #37043 passes a name that already has the namespace togetPrefix. It has to pass the label as the fallback argument.throw errline and the null guard are also in util.inspect: restore the URL arm of formatRaw, with the native class test before the href read #45261 and in util.inspect: print the state of Response, Request, Headers, Blob and six more classes #45355.Against Node.js v26.12.1 (real binary, same scripts).
all 7 as Node.js 26.12.1. Main: exit 1,7 of 7 as Node.js 26.3.0.showHiddenrow that differs by JavaScriptCore's ownline,columnandsourceURLkeys.inspectthrows for 3 rows, the same 3 as on Node.js, with the same error. The rows with a getter that throws leave out the text in the parentheses.errororsuppressed(a throwingSymbol.toStringTag,stack,name,message,causeorerrorsgetter, a revoked Proxy, a throwinginspect.custom): 2 throw, the same 2 as on Node.js, with the same error. Main printed none of these members.node:assertmessages: none throws that did not throw on main.toStringasname,messageorstack, anError.prepareStackTracethat throws or returns no string, and others), each inspected directly and thrown by a getter: none of the 46 rows throws that did not throw on main, and no getter row throws. 12 rows print where main threw.catchof the getter arm.Cost, main against this branch.
perf_event_openover a fixed loop of the built-inutil.inspecton release builds of main and of e797ab5, concurrent JIT off, median of 9 pairings. The commits after e797ab5 change only thecatchof the getter arm, which no row enters:inspect(5)inspect({ a: 1, b: "x" })inspect(new Map([[1, 2]]))inspect(new Set([1]))inspect(new Uint8Array(4))inspect(new Error("x"))causebreakLength: InfinitybreakLength: Infinity, compact: trueinspect(1234567, { numericSeparator: true })inspect(1234.5678, { numericSeparator: true })node_modulesString#lastIndexOf. AnErrorhas 3 moreObject#hasOwnProperty, andstartsWithandslicein place of a RegExpreplace.BUN_JSC_useJIT=0 BUN_JSC_useGC=0,heapStats().objectTypeCounts): no default-option scenario allocates more. The 7 with anErrorallocate 1 to 3 fewer.numericSeparatorwith a fraction allocates 1 more string.formatNumber91 to 110,getPrefix56 to 79,formatError289 to 378,markNodeModules53 to 92,reduceToSingleString198 to 209,formatProperty309 to 316,formatRaw1,049 to 1,041.sizeof the two release binaries gave text 138,010,898 and 138,013,261 (+2,363), with data and bss unchanged and a file 4,096 bytes larger.util.inspect: 1,012 cells (main 1,013). One module-level RegExp fewer.Tests.
test/js/node/util/node-inspect-tests/parallel/util-inspect.test.js: upstream v26.12.1's test blocks at the positions of upstream, and the tests of util.inspect: read Symbol.toStringTag in a try, as Node.js does #46414. Two existing assertions change to Node's text: theSymbol.speciesgetter, which pinned the wrong indent, andFloat16Array.test/js/node/util/node-inspect-tests/internal-inspect.test.js: the tests of util.inspect: do not throw when an error's stack, name, message or errors getter throws #45254, then 10 groups whose inputs are ininternal-inspect-fixture.cjs. One more test runs that fixture under a Node.js that is v26.12.1 or later and compares all 99 rows. One test states Bun's text inside<Inspection threw (...)>. It is skipped where no such Node.js is installed.test/js/node/util/util.test.js: theinspect numericSeparatorblock of fix(node:util): correct numericSeparator for negative fractional numbers #32460. One number literal is written as1.234567891234568e21for the lint ruleno-loss-of-precision.test/js/node/assert/assert.spec.tsandtest/js/node/inherited-errors.ts: from util.inspect: do not throw when an error's stack, name, message or errors getter throws #45254.breakLength: Infinity, which cannot change output.Suites run on the debug build of this branch.
test/js/node/util/: 645 pass.test/js/node/assert/,test/js/node/console/,test/js/bun/console/,test/js/node/events/and 3 files oftest/js/bun/util/: 906 pass. Vendoredtest-assert*,test-console*,test-repl*,test-util*,test-errors-*andtest-event-emitter-errors: 142 of 142. These ran with a 60 s limit per test, because 3 tests oftest/js/node/util/pass the 5 s limit on a debug build of main too.bun lint,bun fmt --checkand the type check of the built-in modules report nothing for the changed files.no test proof · iteration 1 · platform-specific test(s) that do not run on this machine, deferring to CI, which covers all platforms: test/js/node/util/node-inspect-tests/parallel/util-inspect.test.js