Skip to content

ErrorInstance: mark the stack frames while the embedder keeps them alive - #510

Closed
robobun wants to merge 1 commit into
mainfrom
farm/7c4fbefc/keep-error-stack-frames-alive
Closed

ErrorInstance: mark the stack frames while the embedder keeps them alive#510
robobun wants to merge 1 commit into
mainfrom
farm/7c4fbefc/keep-error-stack-frames-alive

Conversation

@robobun

@robobun robobun commented Aug 24, 2026

Copy link
Copy Markdown
Collaborator

Problem

  • ErrorInstance holds its captured frames weakly. Once one frame's callee or CodeBlock is unmarked, ErrorInstance::reconcileWeakReferencesAtGCEnd renders the stack string in the GC end phase through vm.onComputeErrorInfo() and drops the frames.
  • No JS can run in that phase. Bun's Error.prepareStackTrace support lives in onComputeErrorInfoJSValue, which materializeErrorInfoIfNeeded only calls while the frames still exist. So an error whose frames die before the first .stack read skips the user formatter and gets the default string. Every error thrown from an async function hits this once a GC runs between the throw and the first read (the async body runs in a per-call closure).

Fix

  • Add VM::setKeepsErrorStackFramesAlive(bool). While it is set, the new ErrorInstance::visitChildren marks the frames under the cell lock, so a live error keeps them until materializeErrorInfoIfNeeded runs. Exception::visitChildren already marks its own copy of the frames the same way.
  • V8 keeps the captured frames alive until the stack is formatted. Bun sets the flag only while a user Error.prepareStackTrace is installed, so other programs keep the weak frames and the GC end phase rendering.
  • The flag is a relaxed atomic read by marking threads. A stale read only leaves that error's frames weak for the current cycle, which is today's behavior.
  • USE(BUN_JSC_ADDITIONS) only. The matching Bun change sets the flag from the Error.prepareStackTrace setter and adds the tests.

Add VM::setKeepsErrorStackFramesAlive(bool). While it is set,
ErrorInstance::visitChildren marks the captured frames under the cell
lock, so a live error keeps them until materializeErrorInfoIfNeeded
runs. V8 keeps the frames alive the same way.

Bun sets the flag while a user Error.prepareStackTrace is installed.
Without it, a dead frame makes reconcileWeakReferencesAtGCEnd render
the stack string in the GC end phase, where the prepareStackTrace
callback cannot run. The user formatter is then skipped for that error
and error.stack is the default string.

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

I reviewed this PR and didn't find any bugs. Because it adds a visitChildren override that runs on GC marking threads, a human look would still be worthwhile.

What was reviewed:

  • New ErrorInstance::visitChildrenImpl mirrors Exception::visitChildrenImpl (same StackFrame::visitAggregate loop); visitor.vm() is an established accessor.
  • All mutator writes to m_stackTrace in this file already hold cellLock(), so the lock in visitChildren matches the existing concurrency contract.
  • Flag defaults to false, so the added override is a no-op (Base::visitChildren + early return) until the embedder opts in; relaxed atomic read is documented as safe-to-stale.
  • Only JSC-side subclass user is JSDOMErrorWrapper (WebCore, not built in JSCOnly) and it has no visitChildren of its own.
Extended reasoning...

Overview

This PR adds an opt-in mechanism for ErrorInstance to strongly mark its captured StackFrames during GC, instead of holding them weakly. It touches three files: ErrorInstance.cpp gains a visitChildrenImpl (guarded by USE(BUN_JSC_ADDITIONS)) that, when VM::keepsErrorStackFramesAlive() is true, takes the cell lock and calls frame.visitAggregate(visitor) on each captured frame. ErrorInstance.h declares the override. VM.h adds a relaxed std::atomic<bool> m_keepsErrorStackFramesAlive with getter/setter.

Security risks

None identified. This is GC marking logic — no parsing, auth, or external input. The change can only cause objects to be kept alive longer (when the flag is set), which is a memory-retention trade-off the PR description explicitly discusses (V8 parity while Error.prepareStackTrace is installed), not a safety issue. The relaxed atomic is read-only from marking threads and a stale read degrades to today's weak-frame behavior.

Level of scrutiny

Moderate-to-high. visitChildren runs on concurrent marking threads, so mistakes here (missing locks, unguarded pointer reads) can cause hard-to-reproduce crashes. That said, the implementation is a near-verbatim copy of Exception::visitChildrenImpl (Exception.cpp:60-70), with the addition of the cell lock — which is the correct addition, since ErrorInstance::m_stackTrace is a unique_ptr mutated under cellLock() throughout the file (setStackFrames, captureStackTrace, finishCreation, computeErrorInfo, materializeErrorInfoIfNeeded, estimatedSize). The runtime flag defaults to false, so without the matching Bun-side change the new code path is inert.

Other factors

  • CREATE_METHOD_TABLE(ErrorInstance) will now pick up the new visitChildren; the only subclass in the tree (JSDOMErrorWrapper in WebCore) doesn't define its own visitChildren and isn't built for the JSCOnly/Bun port.
  • The added comment in reconcileWeakReferencesAtGCEnd correctly notes that with the flag on, no frame will be found dead there.
  • No bugs surfaced from the automated bug-hunting pass.

I'm deferring rather than approving because GC marking is a critical path where subtle races are possible, and a second pair of eyes on any new visitChildren override is cheap insurance — even though this one follows the established pattern closely.

@coderabbitai

coderabbitai Bot commented Aug 24, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 978e2b2c-c853-498f-9434-467725ca6497

📥 Commits

Reviewing files that changed from the base of the PR and between c148a12 and 8f7ed77.

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

Included review availability: Your plan provides up to 5 included reviews per hour; 0 remain after this review.


Walkthrough

Changes

Error stack frame GC retention

Layer / File(s) Summary
VM retention state
Source/JavaScriptCore/runtime/VM.h
VM stores an atomic flag and provides relaxed-memory-order accessors for error stack frame retention.
ErrorInstance child visitation
Source/JavaScriptCore/runtime/ErrorInstance.h, Source/JavaScriptCore/runtime/ErrorInstance.cpp
ErrorInstance declares and implements conditional GC child visitation. The visitor marks stack-frame aggregates when the VM retains error stack frames.

Merge Risk: 🔵 Low · up to 8f7ed

This change keeps error stack frames alive while custom formatting is enabled, but a stale synchronization read could allow frames to be collected before formatting and produce the default stack string. The PR is mergeable with explicit owner confirmation of the feature guard and GC synchronization behavior.

🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description explains the problem and fix, but it omits the required Bugzilla link, review line, and changed-file/function list. Add the bug title and Bugzilla URL, include the required review line, and list each changed file with the relevant functions or classes.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: keeping captured stack frames marked while the embedder keeps them alive.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Warning

Git: Failed to clone repository. Please run the @coderabbitai full review command to re-trigger a full review. If the issue persists, set path_filters to include or exclude specific files.


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

@github-actions

Copy link
Copy Markdown

Preview Builds

Commit Release Date
8f7ed779 autobuild-preview-pr-510-8f7ed779 2026-08-24 14:36:18 UTC

@robobun

robobun commented Aug 24, 2026

Copy link
Copy Markdown
Collaborator Author

Closing in favor of #511, which marks the frames unconditionally instead of behind VM::setKeepsErrorStackFramesAlive. The unconditional pin also fixes the header loss and the Error.captureStackTrace frame loss that happen with no user Error.prepareStackTrace installed (oven-sh/bun#34398), and it needs no per-realm flag on the Bun side. Bun side: oven-sh/bun#40354 replaces oven-sh/bun#40352.

@robobun robobun closed this Aug 24, 2026
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