Skip to content

error: give errors created with no JS frames a .stack property - #38074

Open
robobun wants to merge 4 commits into
mainfrom
farm/5dcf8dce/frameless-error-stack
Open

error: give errors created with no JS frames a .stack property#38074
robobun wants to merge 4 commits into
mainfrom
farm/5dcf8dce/frameless-error-stack

Conversation

@robobun

@robobun robobun commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator

Problem

  • Any error Bun builds while no JS is running has no stack property at all: typeof err.stack === "undefined", Object.hasOwn(err, "stack") === false. Node always gives an error a .stack string, at minimum the Name: message line. Code doing err.stack.split("\n") throws, and --unhandled-rejections=warn prints UnhandledPromiseRejectionWarning: [object Object] for these rejections because node's error-like check is "has an own stack".
  • Affected (all reproduced on 1.4.0 and main, probe below): every fs callback and fs.promises error, Bun.file().text(), Bun.connect() (the rejection and the connectError argument), fetch() network failures, node:dns, RedisClient connection errors, Bun.password.verify(), the AggregateError from Bun.build() / Bun.Transpiler, the WebSocket error event's .error, node:crypto job errors passed to callbacks. The one exception was an error awaited directly inside an async function, which got a string because an at async f frame was attached.
  • Cause: JSC's ErrorInstance::materializeErrorInfoIfNeeded (vendored ErrorInstance.cpp:410) only defines .stack when the captured frame vector is non-empty. An error created from an event loop callback captures an empty vector, so nothing ever defines the property. Bun__attachAsyncStackFromPromise (src/jsc/bindings/AsyncStackTrace.cpp) fills the vector when an async function is awaiting the promise, but returned without touching the error for .then() / .catch(), combinators, top-level await, and callback or event delivery.

Fix

  • Bun::installLazyStackIfFrameless (FormatStackTraceForJS.cpp): if an error's frame vector exists but is empty and it has no own stack yet, install the lazy stack accessor Error.captureStackTrace already uses. The first read formats the empty vector the normal way (so Error.prepareStackTrace runs, with an empty call-site array) and replaces the accessor with a non-enumerable data property holding Name: message.
  • Called from every function native code constructs an ErrorInstance through: systemErrorToErrorInstance and SystemError__toErrorInstanceWithInfoObject, ErrorCodeCache::createError (all ERR_* errors), the ZigString__to*ErrorInstance / JSC__create*Error string-to-error family (and ZigString__toDOMExceptionInstance, for the codes that yield a real Error), both createAggregateError bindings, S3Error__toErrorInstance, createCryptoError (node:crypto jobs), and WebSocket.cpp's error event. Constructors called during JS execution see a non-empty vector and return after one check, so synchronous errors are untouched. createOutOfMemoryError is deliberately left out.
  • Bun__attachAsyncStackFromPromise also calls it when the await walk recovers nothing (covers any constructor not listed above). It now returns early for a null vector too, which is what the helper does: a null vector means the error was created with Error.stackTraceLimit deleted (V8 leaves .stack undefined in that case) or already holds a stack string; previously it attached frames to those, so a deleted limit behaved differently depending on whether something awaited the promise.
  • Bun.build() rejects through reject_with_async_stack like the other native promise APIs (js_bundle_completion_task.rs), so an awaited failure lists the awaiting function.
  • Why this is the right shape:
    • Matches node: own, non-enumerable .stack; V8 also exposes it as an accessor until first read; formatted lazily, so later name / message changes and Error.prepareStackTrace are honored; Error.stackTraceLimit = 0 yields the header line, a deleted Error.stackTraceLimit yields undefined, in both the .then() and await consumption modes (test matrix below).
    • No stack capture on the success path of any API (the approach fetch: reject network errors as TypeError('fetch failed') with cause, errno codes, and a caller stack #35998 took for fetch, dropped because it allocates an Error per call). Frameless errors pay one property add; output of console.log / uncaught printing for them is byte-identical because the printer reads the frame vector, not the property.
    • Idempotent: the own-property check makes the constructor-time and attach-time calls compose and leaves a user-assigned .stack alone.
  • The uniform fix is one condition in the Bun-owned USE(BUN_JSC_ADDITIONS) block of ErrorInstance::materializeErrorInfoIfNeeded (fn && m_stackTrace && !m_stackTrace->isEmpty() -> fn && m_stackTrace; Bun's hook already formats an empty vector). That needs an oven-sh/WebKit change plus a pin bump, and its fail-before cannot be demonstrated from this repo alone, so this PR does it at Bun's constructors; once the engine change lands, the helper and its call sites can be deleted. Until then the only remaining gap is Error.stackTraceLimit = 0; new Error(), which is constructed inside JSC.
  • Replaces SystemError: give async fs/dns/etc. errors a .stack string when there are no JS frames #35515 (SystemError site only) and error: give native promise-rejection errors a .stack when no frames exist #35989 (attach site only), both closed.
  • Verified (every new assertion fails on the released binary with USE_SYSTEM_BUN=1 and passes with bun bd test; the two tests below that pin unchanged behavior are marked):
    • test/js/node/v8/capture-stack-trace.test.js, new describe: fs callback error (callback delivery): own + non-enumerable, lazy formatting, prepareStackTrace with zero call sites, assignment; fetch() via .then() gets the header, via await still gets the async frame (unchanged behavior); Bun.build() via .then() and via await; Error.stackTraceLimit = 0 for a SystemError and an ERR_* error; deleted limit on a sync error (unchanged behavior); the {10, 0, deleted} x {.then(), await} matrix on Bun.password.verify() (plain constructor + attach); --unhandled-rejections=warn prints the error.
    • test/js/node/fs/promises.test.js: the Promise-subclass / thenable / Promise.all tests used to accept undefined, now require the header; new .catch() test.
    • test/js/valkey/reliability/connection-failures.test.ts: ERR_REDIS_CONNECTION_CLOSED via the error-code constructor (plain reject, no attach); needs no server.
    • test/js/web/websocket/error-event.test.ts: event.error of a refused connection.
    • test/js/node/crypto/crypto-sign-regression.test.ts: crypto.sign() callback error from a failing job.
    • Also green with the change: bun-file, streams, s3 (local error paths), transpiler error tests, domexception, error-code-mirror, websocket close/handshake, fs promises, and node's crypto sign/keygen, prepare-stack-trace and unhandled-rejection-warning tests. inspect-error.test.js has two failures in a debug build that reproduce without this change.

Background

  • Frame vector and materialization: a JSC ErrorInstance stores the frames captured at construction in m_stackTrace and only defines the stack / line / column properties on the first access to one of them (materializeErrorInfoIfNeeded), formatting through Bun's hook. With zero frames that code is skipped entirely, which is the bug. getStackTrace returns an empty vector when there was nothing to capture and a null one when Error.stackTraceLimit is deleted or not a number; errors rebuilt by structuredClone also have a null vector and carry a stack string instead.
  • Lazy stack accessor: errorInstanceLazyStackCustomGetter (FormatStackTraceForJS.cpp) is a CustomGetterSetter Bun installs as an own stack property; reading it formats whatever frames the error holds at that moment and putDirects the result over itself. Until now only Error.captureStackTrace installed it.
  • Async stack attach: errors created by event loop tasks are passed to Bun__attachAsyncStackFromPromise, which walks the promise's reaction chain looking for async functions awaiting it and stores those as at async f frames. Consumers that are not a direct await (.then(), Promise.all, top-level await, callbacks) give it nothing to find.
  • SystemError: the Rust-side struct for errno-style errors (code / syscall / path / errno); systemErrorToErrorInstance turns it into a JS Error. ERR_* errors are node-style coded errors, all built by ErrorCodeCache::createError. Message-only errors from Rust go through the ZigString__to*ErrorInstance / JSC__create*Error bindings.
Probe: native errors with and without .stack (main before / after)

typeof e.stack before, on main: fs.promises await / .then() / .catch(), fs.readFile / open / stat callbacks, createReadStream error event, Bun.file().text() / .arrayBuffer(), Bun.connect (await, .then(), connectError argument, rejection after connectError), fetch (await, .catch()), dns.lookup callback, dns.promises.lookup, dns.resolve4 callback, RedisClient, Bun.password.verify via .then(), Bun.build (.then() and awaited), Bun.Transpiler.transform, WebSocket error event, crypto.sign callback: all undefined. zlib callbacks, Bun.spawn ENOENT, Bun.SQL, Bun.file().stream(), aborted fs.promises.readFile: string (created with JS on the stack).

After: every row above is a string; the awaited Bun.build row additionally has the at async frame.

Bun.password.verify() rejection, .stack by Error.stackTraceLimit and consumer:

limit .then() before await before .then() after await after
10 undefined header + frame header header + frame
0 undefined undefined header header
deleted undefined header + frame undefined undefined
$ bun --unhandled-rejections=warn -e 'require("fs/promises").readFile("/nonexistent")'
before: (node:1) UnhandledPromiseRejectionWarning: [object Object]
after:  (node:1) UnhandledPromiseRejectionWarning: Error: ENOENT: no such file or directory, open '/nonexistent'
Earlier revision

The first revision of this PR called the helper from three sites only (SystemError constructor, ErrorCodeCache::createError, the attach fallback) and described Bun.build's AggregateError and the WebSocket error event as needing an engine change; both are built by Bun's own C++, so review of that revision led to hooking every constructor, the null-vector consistency fix in the attach, and the Bun.build / WebSocket / crypto / matrix tests. The Bun.password tests moved from password.test.ts into the matrix.

JSC only defines .stack when the captured trace has at least one frame.
Errors Bun builds while no JS is running (fs, dns, socket and fetch
completions, ERR_* errors from event loop callbacks, thread pool jobs)
capture none, so they had no .stack at all unless an async function
happened to be awaiting the promise they were rejected with.

Install the lazy .stack accessor that Error.captureStackTrace already
uses on such errors, from the SystemError and error-code constructors
and from the async stack attach when it recovers no frames. Reading it
formats the usual "Name: message" line (through Error.prepareStackTrace
when set), as V8 does for an error with no frames.
@coderabbitai

coderabbitai Bot commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@robobun, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 2 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 5818125b-8e71-4913-a501-6ae8e3277c46

📥 Commits

Reviewing files that changed from the base of the PR and between b7a0431 and 442477c.

📒 Files selected for processing (14)
  • src/jsc/bindings/AsyncStackTrace.cpp
  • src/jsc/bindings/ErrorCode.cpp
  • src/jsc/bindings/FormatStackTraceForJS.cpp
  • src/jsc/bindings/FormatStackTraceForJS.h
  • src/jsc/bindings/S3Error.cpp
  • src/jsc/bindings/bindings.cpp
  • src/jsc/bindings/node/crypto/CryptoUtil.cpp
  • src/jsc/bindings/webcore/WebSocket.cpp
  • src/runtime/api/js_bundle_completion_task.rs
  • test/js/node/crypto/crypto-sign-regression.test.ts
  • test/js/node/fs/promises.test.js
  • test/js/node/v8/capture-stack-trace.test.js
  • test/js/valkey/reliability/connection-failures.test.ts
  • test/js/web/websocket/error-event.test.ts

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

@robobun

robobun commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator Author

Reproduced on the released 1.4.0 binary and on main: errors Bun creates while no JS is running (fs callbacks and fs.promises, Bun.file(), Bun.connect(), fetch(), node:dns, RedisClient, Bun.password, the AggregateError from Bun.build(), the WebSocket error event, node:crypto job callbacks) had typeof err.stack === "undefined" unless the promise was awaited directly inside an async function; node gives every one of them at least the Name: message line.

Fix is in this PR (#38074): every constructor native code builds an error through installs the lazy .stack accessor when the error captured zero frames, and the async-stack attach does the same when it finds nothing to attach. Tests in test/js/node/v8/capture-stack-trace.test.js, test/js/node/fs/promises.test.js, test/js/valkey/reliability/connection-failures.test.ts, test/js/web/websocket/error-event.test.ts and test/js/node/crypto/crypto-sign-regression.test.ts fail on the released binary and pass with the fix. Replaces #35515 and #35989.

@github-actions

Copy link
Copy Markdown
Contributor

This PR may be a duplicate of:

  1. SystemError: give async fs/dns/etc. errors a .stack string when there are no JS frames #35515 - Installs the same lazy .stack accessor on frameless ErrorInstances in SystemError__toErrorInstance (src/jsc/bindings/bindings.cpp), one of the three call sites this PR patches.
  2. error: give native promise-rejection errors a .stack when no frames exist #35989 - Installs the same lazy .stack accessor in Bun__attachAsyncStackFromPromise's empty-frames branch (src/jsc/bindings/AsyncStackTrace.cpp), another of the three call sites this PR patches.

🤖 Generated with Claude Code

@robobun

robobun commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator Author

Those two are partial versions of this change rather than alternatives to it, and this PR is meant to replace both:

This PR covers all three sites through one shared helper. Both older PRs have been conflicting with main for a while; closing them in favor of this one.

Comment thread test/js/node/v8/capture-stack-trace.test.js Outdated
Comment thread src/jsc/bindings/AsyncStackTrace.cpp Outdated
Comment thread src/jsc/bindings/ErrorCode.cpp Outdated
Comment thread src/jsc/bindings/FormatStackTraceForJS.cpp Outdated
Comment thread src/jsc/bindings/FormatStackTraceForJS.h Outdated
Comment thread src/jsc/bindings/bindings.cpp Outdated

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

I reviewed this PR and didn't find any bugs; the earlier stdout-drain nit is now addressed. Because it changes how .stack is installed on every SystemError / ERR_* error / async-attach path via JSC ErrorInstance internals, a human look would still be worthwhile.

What was reviewed:

  • installLazyStackIfFrameless: null-vs-empty stackTrace() gating, getDirect idempotency check, and reuse of the existing m_lazyStackCustomGetterSetter — matches the Error.captureStackTrace install path.
  • uncheckedDowncast<ErrorInstance> in systemErrorToErrorInstanceJSC::createError(globalObject, errorType, ...) always returns an ErrorInstance, so the unchecked cast is sound.
  • Bun__attachAsyncStackFromPromise restructuring — the frames-found path is unchanged (setStackFrames + return); the fallback only runs when limit == 0 or the walk found nothing.
  • New tests: hermetic (tempDir / ephemeral local port), restore Error.stackTraceLimit / prepareStackTrace in finally / afterEach, and the subprocess test now drains stdout.
Extended reasoning...

Overview

The PR adds a single helper, Bun::installLazyStackIfFrameless, in FormatStackTraceForJS.{cpp,h} and calls it from three native-error construction sites: systemErrorToErrorInstance (bindings.cpp), ErrorCodeCache::createError (ErrorCode.cpp), and the empty-frames fallback of Bun__attachAsyncStackFromPromise (AsyncStackTrace.cpp). The helper installs the pre-existing lazy stack CustomGetterSetter on an ErrorInstance when its captured frame vector is present-but-empty and no own stack exists yet. Four test files gain coverage; three pre-existing tests in promises.test.js are tightened from "string or undefined" to "starts with Error: message".

Security risks

None identified. The change only affects the shape of the .stack property on error objects (adding a header-line string where there was previously undefined). No untrusted input parsing, no auth/crypto/permissions surface.

Level of scrutiny

Medium-high. While the helper itself is small and reuses an existing accessor mechanism, it is wired into three very hot construction paths that produce essentially every native error in the runtime (fs, dns, fetch, sockets, redis, all ERR_* codes). It relies on subtle ErrorInstance internals — the distinction between a null m_stackTrace (deleted stackTraceLimit) and an empty one, and the interaction between the installed CustomAccessor and JSC's own materializeErrorInfoIfNeeded. The PR description explicitly notes this is a call-site workaround for behavior that ideally lives inside JSC's materialization; a maintainer should confirm that trade-off is acceptable.

Other factors

  • My prior inline nit (undrained stdout in the --unhandled-rejections=warn subprocess test) was addressed in commit cc66d96; the test now drains all three of stdout/stderr/exited concurrently and asserts { stdout: "", exitCode: 0 }.
  • The comment-cop bot flagged the new 2–4 line explanatory comments at each call site. Those read as concise "why" context (e.g. "Native code also builds these while no JS is running…"), not workaround justifications, but a maintainer may still want them trimmed.
  • The bug-hunting system found nothing this run. I checked that the AsyncStackTrace.cpp restructuring preserves the original setStackFrames path exactly when frames are recovered, that the getDirect guard makes the constructor-time and attach-time calls compose without double-installing, and that the ErrorCodeCache::createError call site is only reached after created_error is confirmed non-null (the earlier if (!created_error) branch handles the exception case).
  • Test coverage is thorough (own/non-enumerable descriptor, lazy formatting, prepareStackTrace with zero call sites, assignment before read, stackTraceLimit = 0 vs. deleted, async-frame preservation, and the --unhandled-rejections=warn end-to-end), and existing weaker assertions were tightened rather than left permissive.

Given the breadth of affected error paths and the JSC-internals dependency, I'm deferring rather than approving.

@robobun

robobun commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 5:05 AM PT - Aug 13th, 2026

@robobun, your commit 442477c has some failures in Build #94627 (All Failures)


🧪   To try this PR locally:

bunx bun-pr 38074

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

bun-38074 --bun

Comment thread src/jsc/bindings/FormatStackTraceForJS.cpp Outdated
Comment thread src/jsc/bindings/FormatStackTraceForJS.h Outdated

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

I reviewed this PR and didn't find any bugs (the earlier stdout-draining nit was addressed in cc66d96). Since it changes error-construction behavior across three native JSC binding sites and interacts with ErrorInstance materialization state, a human look would still be worthwhile.

What was reviewed:

  • installLazyStackIfFrameless: null-vs-empty trace guard, getDirect idempotence check, and reuse of the existing m_lazyStackCustomGetterSetter accessor.
  • The three call sites: uncheckedDowncast<ErrorInstance> in systemErrorToErrorInstance is safe (JSC's createError(_, ErrorType, _) always returns an ErrorInstance); ErrorCodeCache::createError passes the ErrorInstance* directly; the attach-site fallback preserves the existing setStackFrames path when frames are recovered.
  • Tests: cover callback delivery, .then(), await, stackTraceLimit=0 / deleted, prepareStackTrace, and assignment; the tightened promises.test.js assertions no longer accept undefined.
Extended reasoning...

Overview

This PR adds Bun::installLazyStackIfFrameless in FormatStackTraceForJS.cpp and calls it from three native error-construction sites (systemErrorToErrorInstance in bindings.cpp, ErrorCodeCache::createError in ErrorCode.cpp, and the empty-frames fallback in Bun__attachAsyncStackFromPromise). When an ErrorInstance was created with an empty (but non-null) frame vector and has no own .stack yet, it installs the same lazy CustomGetterSetter that Error.captureStackTrace already uses, so .stack reads as "Name: message" and honors Error.prepareStackTrace. Four test files gain coverage for callback-delivered errors, .then() rejections, stackTraceLimit interactions, and the --unhandled-rejections=warn output.

Security risks

None identified. The change reads ErrorInstance::stackTrace() and does a getDirect / putDirectCustomAccessor on a freshly-constructed error; no user-controlled input reaches new parsing or allocation paths.

Level of scrutiny

This is native C++ in the JSC bindings layer, on paths every SystemError and every ERR_*-coded error passes through. It interacts with ErrorInstance's lazy-materialization state (m_stackTrace, hasMaterializedErrorInfo, the lazy-stack accessor) — an area with a history of subtle GC / desync bugs (see the neighboring captureStackTrace branch that avoids setStackFrames after materialization). While the helper itself is small and reuses an existing accessor, the blast radius (every native error) and the subtlety of the invariants argue for a maintainer's review rather than an auto-approve.

Other factors

  • The change is well-tested: nine new tests in capture-stack-trace.test.js covering ownership/enumerability, lazy formatting, prepareStackTrace, assignment, the async-frame path being unchanged, stackTraceLimit=0, deleted stackTraceLimit, and the unhandled-rejection warning; three tightened assertions in promises.test.js; and per-constructor coverage in password.test.ts and connection-failures.test.ts. The PR description confirms USE_SYSTEM_BUN=1 failure / bun bd pass for each.
  • I checked that uncheckedDowncast<ErrorInstance> in systemErrorToErrorInstance is safe: JSC's createError(JSGlobalObject*, ErrorType, const String&) dispatches to constructors that always return ErrorInstances.
  • The remaining comment-cop bot flags on FormatStackTraceForJS.{cpp,h} point at two-line comments that state non-obvious invariants (null vs. empty trace, V8's deleted-stackTraceLimit behavior); after the trim in f904787 they read as appropriate, not paragraph-length justifications.
  • My earlier inline nit about draining stdout in the --unhandled-rejections=warn subprocess test was addressed in cc66d96 and the thread is resolved.

Hook the remaining constructors native code builds errors through
(BunString/ZigString error family, AggregateError, S3Error, the
SystemError info-object variant, node:crypto job errors, the WebSocket
error event) instead of three hand-picked sites, and make the async
stack attach leave errors with a null trace alone (Error.stackTraceLimit
deleted, or a stack string already present), matching what the helper
does. Bun.build now rejects through the async stack attach like the
other native promise APIs.

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

I reviewed this PR (including the expanded 442477c commit) and didn't find any bugs. Because it changes error construction across ~13 native constructor paths in core JSC bindings — and 442477c grew the scope well beyond the three sites described in the PR body — a human look at the overall approach would be worthwhile.

Checked: installLazyStackIfFrameless is idempotent (own-stack guard) so the constructor-site + attach-site calls compose; the AsyncStackTrace.cpp restructure preserves the frames-found path exactly and only falls through when limit == 0 or no frames were recovered; the JSValue overload's dynamicDowncast makes the createDOMException wrap a no-op when the result isn't an ErrorInstance; ordering in S3Error.cpp (accessor installed before .name is set) is fine because the getter reads name lazily.

The Windows bake/deinitialization.test.ts segfault in CI is in a file this PR doesn't touch.

Extended reasoning...

Overview

Adds Bun::installLazyStackIfFrameless (two overloads) in FormatStackTraceForJS.{cpp,h} and threads it through native error construction so errors built with no JS on the stack get an own, non-enumerable, lazily-formatted .stack (matching V8's "Name: message" header behavior). The latest commit (442477c) expanded the original three call sites to every native error constructor reachable from bindings.cpp (ZigString__to{,Type,Syntax,Range}ErrorInstance, ZigString__toDOMExceptionInstance, JSC__create{,Type,Range}Error, both AggregateError helpers, both SystemError paths), plus S3Error, WebSocket error events, and createCryptoError. It also restructures Bun__attachAsyncStackFromPromise to fall through to the helper when no async frames are recovered, and switches Bun.build()'s failure reject to reject_with_async_stack. Tests are added/tightened across seven files.

Security risks

None identified. No parsing of untrusted input, no auth/crypto logic changes (the CryptoUtil.cpp touch only adds the accessor to an already-constructed error object). getDirect and putDirectCustomAccessor don't run user JS, so no reentrancy on the install path.

Level of scrutiny

High. This is not a mechanical change: it touches the construction path of essentially every native error object in the runtime, sits in hand-written JSC binding C++, and the most recent commit roughly quadrupled the number of touched call sites relative to the PR description. The helper itself is small and well-guarded (null trace → skip; non-empty trace → skip; existing own stack → skip), and the lazy getter it installs is the same one Error.captureStackTrace already uses, so the mechanism is proven — but the breadth of application and the design choice (patch every construction site vs. fix JSC's materializeErrorInfoIfNeeded) warrant a maintainer's sign-off.

Other factors

  • My earlier inline nit (undrained stdout pipe) was addressed in cc66d96.
  • The two unresolved comment-cop threads on FormatStackTraceForJS.{cpp,h} appear to be bot noise — the flagged comments are already one-liners after f904787.
  • Test coverage is thorough: property descriptor shape, lazy formatting, prepareStackTrace with zero call sites, stackTraceLimit = 0 vs deleted, the --unhandled-rejections=warn end-to-end, and per-constructor coverage (fs callback, fetch, Bun.build AggregateError, Bun.password, redis, crypto sign job, WebSocket error event).
  • CI shows one failure (test/bake/deinitialization.test.ts segfault on Windows x64) at f904787; that test is unrelated to anything this PR touches, and there's a newer commit (442477c) whose CI status isn't in the timeline yet.

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.

1 participant