Skip to content

[JSC] addErrorInfo() drops what the host's stack formatter throws - #774

Merged
sosukesuzuki merged 1 commit into
mainfrom
claude/add-error-info-exception-check
Oct 6, 2026
Merged

sosukesuzuki merged 1 commit into
mainfrom
claude/add-error-info-exception-check

Conversation

@sosukesuzuki

Copy link
Copy Markdown
Member

What

With a host that formats stacks (VM::onComputeErrorInfoJSValue; Bun), the fork's addErrorInfo() runs the formatter while the parser's error object is being made. The formatter can run Error.prepareStackTrace, so it can throw, and its ThrowScope asks its callers for an exception check either way.

The Function constructor makes no such check. constructFunctionSkippingEvalEnabledCheck() gets the error back from FunctionExecutable::fromGlobalCode() and throws it. So:

  • new Function("(") aborts a build run with --validateExceptionChecks=1 (Bun's ASAN lane sets BUN_JSC_validateExceptionChecks=1):

    ERROR: Unchecked JS exception:
        This scope can throw a JS exception: computeErrorInfoToJSValueWithoutSkipping @ src/jsc/bindings/FormatStackTraceForJS.cpp:583
        But the exception was unchecked as of this scope: constructFunctionSkippingEvalEnabledCheck @ Source/JavaScriptCore/runtime/FunctionConstructor.cpp:220
    ASSERTION FAILED: exception check validation failed
    
  • when Error.prepareStackTrace throws, that exception is still pending while the SyntaxError is thrown over it.

eval and vm.Script do check after the error is made, which is why only the Function constructor trips.

Fix

addErrorInfo() catches around the formatter and clears what it threw, short of a termination. The caller made the error to throw it, so the SyntaxError is what the program sees.

Behaviour does not change. With an Error.prepareStackTrace that throws, new Function("("), (0, eval)("(") and new vm.Script("(") each throw the SyntaxError before and after this change (Node 22 does the same).

Verification

Bun built against this tree (debug + ASAN, Linux x64), with BUN_JSC_validateExceptionChecks=1:

  • new Function("(") throws a SyntaxError and no longer aborts, with the default formatter and with an Error.prepareStackTrace that throws.
  • The two new Function assertions that Upgrade WebKit to dbdca7545d bun#44615 keeps as a test.todo in test/js/bun/jsc/webkit-upgrade-dbdca7545d.test.ts pass.
  • capture-stack-trace.test.js, inspect-error.test.js and vm.test.ts pass.

There is no JSTests case: the jsc shell has no stack formatting hook, so this path does not exist there. The test is the one in Bun, which stops being a todo when Bun's WebKit pin includes this.

With a host that formats stacks (VM::onComputeErrorInfoJSValue; Bun),
addErrorInfo() runs the formatter while the parser's error object is being
made. The formatter can run Error.prepareStackTrace, so it can throw, and its
ThrowScope asks its callers for an exception check either way. The Function
constructor makes no such check: constructFunctionSkippingEvalEnabledCheck()
gets the error back and throws it, so `new Function("(")` aborts under
--validateExceptionChecks=1, and a throw from Error.prepareStackTrace was
pending while the SyntaxError was thrown over it.

addErrorInfo() now catches around the formatter and clears what it threw,
short of a termination. The caller made the error to throw it: the
SyntaxError is what the program sees, as it already did, through the Function
constructor, eval and vm.Script alike.

@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 found no bugs; since it changes exception-scope handling in JSC's error-construction path (a CODEOWNERS-covered area with no in-tree test for the host-formatter path), a human look would still be worthwhile.

What was reviewed:

  • The restructured branch: hostComputes == false and non-Bun builds still reach the same materializeErrorInfoIfNeeded(vm) call and putDirect fallthrough as before — no behavior change there.
  • Choice of DECLARE_TOP_EXCEPTION_SCOPE: this tree has no CatchScope; TopExceptionScope is the fork's catch idiom (same as createTypeErrorCopy in the same file) and nests correctly inside callers' scopes.
  • Whether the new scope's constructor could itself trip validateExceptionChecks on an earlier unchecked throw — ruled out, since the host formatter's own ThrowScope constructor already performed that same verification before this change.
  • Whether dropping a throwing Error.prepareStackTrace changes what programs observe — ruled out: previously the SyntaxError was thrown over the pending exception anyway, so the observable result (SyntaxError) is the same.
Extended reasoning...

The change is a single-file, roughly ten-line edit in Source/JavaScriptCore/runtime/Error.cpp inside the USE(BUN_JSC_ADDITIONS) branch of addErrorInfo(), wrapping the host stack formatter call in a TopExceptionScope and clearing non-termination exceptions before returning. It touches no injection, auth, or data-exposure surface. The code is correct as far as I can verify against TopExceptionScope, ThrowScope, and ErrorInstance::materializeErrorInfoIfNeeded, and the hostComputes==false path is unchanged. Deferring rather than approving because /Source/JavaScriptCore is covered by .github/CODEOWNERS, the change alters exception-propagation semantics in a core engine path, and the only test coverage lives in the Bun repository rather than in this tree.

@github-actions

github-actions Bot commented Oct 6, 2026

Copy link
Copy Markdown

Preview build of 6bd59af: autobuild-preview-pr-774-6bd59af3

@sosukesuzuki
sosukesuzuki merged commit 7012d42 into main Oct 6, 2026
49 checks passed
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