Skip to content

ErrorInstance: pass the instance to the finalizer-time error-info hook (onComputeErrorInfoWithErrorInstance) - #445

Closed
robobun wants to merge 1 commit into
mainfrom
farm/5912a43b/error-info-with-error-instance
Closed

ErrorInstance: pass the instance to the finalizer-time error-info hook (onComputeErrorInfoWithErrorInstance)#445
robobun wants to merge 1 commit into
mainfrom
farm/5912a43b/error-info-with-error-instance

Conversation

@robobun

@robobun robobun commented Aug 15, 2026

Copy link
Copy Markdown
Collaborator

Problem

  • ErrorInstance holds its captured stack frames weakly. When a GC finds that one of them died before .stack was read (a callee closure nothing references anymore, a CodeBlock jettisoned for old age), ErrorInstance::finalizeUnconditionally flushes the frames to a string right there, through vm.onComputeErrorInfo() (ErrorInstance.cpp, computeErrorInfo).
  • That hook gets the frames but not the error, so the embedder cannot put the error's name and message on the first line of the string. Bun's V8-style formatter hardcodes Error there, so an error kept alive across a GC reads back .stack as "Error\n at ..." instead of "TypeError: <message>\n at ...". The same error formats correctly if no GC happens in between, which is what makes it show up as flaky (first seen in bun's express res.sendFile test, where finalhandler renders err.stack into the response body).
  • onComputeErrorInfoJSValue(), the hook used when the frames are still alive, already receives the instance; only the finalizer-time hook lacks it.

Fix

  • Add VM::onComputeErrorInfoWithErrorInstance() / setOnComputeErrorInfoWithErrorInstance(): same signature as onComputeErrorInfo() plus the ErrorInstance (as JSObject*, mirroring ErrorInfoFunctionJSValue).
  • ErrorInstance::computeErrorInfo prefers it when set and otherwise falls back to onComputeErrorInfo() as before.
  • Additive on purpose: bun main (which only installs the old hook) keeps building against and behaving the same with this change, so it can be picked up by a routine bump and the bun side can switch over separately. Once bun only installs the new hook, onComputeErrorInfo() can be deleted.
  • The hook still runs from the GC's unconditional finalizers, so the embedder side must not allocate on the JS heap; the comment on the typedef says so. Bun's implementation reads name / message off the instance's property storage with Structure::getConcurrently + JSString::tryGetValueWithoutGC, the same way it already reads function names on this path.

The bun side (installing the new hook and the regression test) is in a separate oven-sh/bun PR that bumps WEBKIT_VERSION to this change.

… (onComputeErrorInfoWithErrorInstance)

ErrorInstance keeps its captured stack frames weakly. When a GC finds
that one of them died (a callee closure that is no longer reachable, a
CodeBlock jettisoned for old age) before .stack was ever read,
finalizeUnconditionally() flushes the frames to a string through
vm.onComputeErrorInfo(). That hook receives only the frames, not the
error, so the embedder cannot render the error's name and message into
the header line of the string. Bun's V8-style formatter therefore
hardcodes "Error" there, and an error that was kept around across a GC
ends up with a .stack of "Error\n    at ..." instead of
"TypeError: <message>\n    at ..." (the same error's .stack is right
whenever no GC runs in between). onComputeErrorInfoJSValue(), the hook
used when the frames are still alive, already receives the instance.

Add onComputeErrorInfoWithErrorInstance(), which is the same as
onComputeErrorInfo() plus the ErrorInstance, and prefer it in
computeErrorInfo() when it is set. The old hook is left in place and
still used as the fallback so that a Bun that has not switched over yet
keeps building and behaving as before; it can be removed once Bun only
installs the new one.
@coderabbitai

coderabbitai Bot commented Aug 15, 2026

Copy link
Copy Markdown

Warning

Review limit reached

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

Next review available in: 4 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: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: e333601f-d27f-4a06-8297-8cf6ae70956a

📥 Commits

Reviewing files that changed from the base of the PR and between 592a255 and 55cf7ef.

📒 Files selected for processing (2)
  • Source/JavaScriptCore/runtime/ErrorInstance.cpp
  • Source/JavaScriptCore/runtime/VM.h

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

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — small additive hook that mirrors the existing ErrorInfoFunctionJSValue pattern.

What was reviewed:

  • New hook is preferred only when set; onComputeErrorInfo() fallback and the no-hook stackTraceAsString path are unchanged, so current Bun builds behave identically.
  • Passing this on the finalizeUnconditionally path is fine — the instance is live there (same path already passes this->bunErrorData() and holds this in BunErrorInstanceFinalizer); the no-JS-heap-allocation constraint is documented on the typedef.
  • Getter/setter/member additions in VM.h are copy-paste of the sibling hooks and stay under USE(BUN_JSC_ADDITIONS).
Extended reasoning...

Overview

Adds a second variant of the finalizer-time error-info hook, VM::onComputeErrorInfoWithErrorInstance(), whose signature is identical to the existing onComputeErrorInfo() plus a JSObject* errorInstance parameter. ErrorInstance::computeErrorInfo prefers the new hook when set and falls back to the old one otherwise. Touches two files: a typedef + getter/setter/member in VM.h (all under USE(BUN_JSC_ADDITIONS)), and a 4-line branch in ErrorInstance.cpp.

Security risks

None. This is embedder plumbing: a WTF::Function slot on VM and one extra branch at the existing call site. No parsing, no untrusted input, no allocation-pattern changes. The JSObject* passed is this, which is already in scope and already partially exposed on this path via this->bunErrorData().

Level of scrutiny

Low-to-moderate. The touchpoint (finalizeUnconditionallycomputeErrorInfo) is GC-adjacent, which normally warrants care, but the change does not alter what runs there — it only threads an extra pointer to the same hook shape that already exists (ErrorInfoFunctionJSValue already takes JSObject*). The ErrorInstance is guaranteed live during its own unconditional finalizer, and the no-JS-heap-allocation constraint that already applied to onComputeErrorInfo is restated on the new typedef. Because the new hook is unset until the companion Bun PR lands, this change is behaviorally a no-op on its own.

Other factors

  • Purely additive and backward-compatible by design (PR description calls this out; verified in the diff that the fn fallback and the m_stackPropertyAlreadyMaterialized short-circuit are preserved).
  • Follows the exact pattern of the three sibling hooks (onComputeErrorInfo, onComputeErrorInfoJSValue, onAppendStackTrace) — same const/non-const accessor pair, same WTF::move setter, same member placement.
  • The upstream CODEOWNERS entry for Source/JavaScriptCore names an Apple reviewer team that does not apply in this fork; recent history shows Bun-specific JSC changes merged routinely.

@github-actions

Copy link
Copy Markdown

Preview Builds

Commit Release Date
55cf7efb autobuild-preview-pr-445-55cf7efb 2026-08-15 13:51:44 UTC

@robobun

robobun commented Aug 15, 2026

Copy link
Copy Markdown
Collaborator Author

Closing: this duplicates #302, which adds the same callback (onComputeErrorInfoWithInstance) for the same reason and has the bun side waiting in oven-sh/bun#34408. I had not found it before opening this.

@robobun robobun closed this Aug 15, 2026
@robobun
robobun deleted the farm/5912a43b/error-info-with-error-instance branch August 15, 2026 15:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant