Report exceptions from DeferredWorkTimer tasks as uncaught#30835
Report exceptions from DeferredWorkTimer tasks as uncaught#30835robobun wants to merge 6 commits into
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (1)
💤 Files with no reviewable changes (1)
WalkthroughThis PR adds exception handling for deferred JavaScript tasks in the JSC binding layer, wraps those changes with integration tests for FinalizationRegistry cleanup callbacks, and updates overlay CSS embedding in the build codegen to use JSON.stringify encoding. ChangesException handling and build updates
Suggested Reviewers
🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Comment |
|
Updated 7:27 AM PT - May 22nd, 2026
❌ @robobun, your commit 5d967d6 has some failures in 🧪 To try this PR locally: bunx bun-pr 30835That installs a local version of the PR into your bun-30835 --bun |
|
This PR may be a duplicate of:
🤖 Generated with Claude Code |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@test/js/bun/util/finalization-registry-throwing-callback.test.ts`:
- Line 26: Remove the panic-style negative stderr assertion by deleting the
expect(err).not.toContain("ASSERTION"); check in the test file
finalization-registry-throwing-callback.test.ts and instead rely on existing
positive behavior assertions (or add explicit positive assertions about the
expected output/side-effects) so the test no longer asserts absence of
"ASSERTION" and only verifies correct, stable outcomes.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 5381e193-ca8c-4f29-8d64-3d75a3b2a3f5
📒 Files selected for processing (12)
src/crash_handler/lib.rssrc/errno/lib.rssrc/jsc/bindings/JSCTaskScheduler.cppsrc/perf/tracy.rssrc/runtime/cli/Arguments.rssrc/runtime/cli/run_command.rssrc/runtime/cli/upgrade_command.rssrc/runtime/jsc_hooks.rssrc/runtime/webview/ChromeProcess.rssrc/spawn/process.rssrc/spawn_sys/spawn_process.rstest/js/bun/util/finalization-registry-throwing-callback.test.ts
| import { test, expect } from "bun:test"; | ||
| import { bunEnv, bunExe } from "harness"; | ||
|
|
||
| test("FinalizationRegistry callback that throws does not crash the process", async () => { |
There was a problem hiding this comment.
🟡 nit: per test/CLAUDE.md, tests in the same file that spawn independent subprocesses should use test.concurrent (or wrap in describe.concurrent). Both tests here spawn standalone Bun processes with no shared state, so they can run in parallel — applies to this test and the one at line 31.
Extended reasoning...
What
test/CLAUDE.md (line 22) documents a repo convention:
Prefer concurrent tests over sequential tests: When multiple tests in the same file spawn processes or write files, make them concurrent with
test.concurrentordescribe.concurrentunless it's very difficult to make them concurrent.
This new file declares two tests with plain test(...):
"FinalizationRegistry callback that throws does not crash the process"(line 4)"FinalizationRegistry callback that throws is catchable via uncaughtException"(line 31)
Both spawn a fresh Bun subprocess via Bun.spawn, read its stdio, and assert on the result.
Why this applies here
Walking through the two tests concretely:
- Test 1 spawns
bun -e <script>withstderr: "pipe", awaitsproc.stderr.text()andproc.exited, and asserts on those values. - Test 2 spawns a different
bun -e <script>with its ownstdout/stderrpipes and asserts on those.
There is no shared mutable state between them — no shared temp dir, no shared port, no module-level fixture, no ordering dependency. Each subprocess is hermetic. Running them sequentially is purely wasted wall-clock time; running them concurrently cannot change their outcome.
Why nothing prevents it
The tests are correct as written; this is purely a project-convention / CI-throughput concern. Nothing in the harness enforces test.concurrent, so it has to be caught in review.
Impact
Minor. The two subprocesses are tiny and exit quickly, so the wall-clock saving is small. This is flagged only because it's an explicit, documented convention for new test files in this repo.
Fix
Either change both declarations to test.concurrent(...), or wrap them in describe.concurrent("FinalizationRegistry throwing callback", () => { ... }) and keep the inner test(...) calls.
7b389b3 to
65006f9
Compare
65006f9 to
93bd662
Compare
| IS_ERROR_RUNTIME: String(file === "error"), | ||
| IS_BUN_DEVELOPMENT: String(!!debug), | ||
| OVERLAY_CSS: css("../runtime/bake/client/overlay.css", !!debug), | ||
| OVERLAY_CSS: JSON.stringify(css("../runtime/bake/client/overlay.css", !!debug)), |
There was a problem hiding this comment.
🟡 Wrapping OVERLAY_CSS in JSON.stringify() means bake-codegen.ts no longer passes raw minified CSS verbatim as a define value, so the explanatory comments at src/parsers/json_lexer.rs:1302-1306 and test/bundler/bun-build-api.test.ts:73-76 — which both cite "bake-codegen.ts's OVERLAY_CSS" as the motivating example for auto-quoting define values that start with *{...} — are now factually stale. The lexer special-case and test remain valid for user-supplied raw-CSS define values; only the bake-codegen citation needs updating (or just dropping the example). Also worth noting: this drive-by build fix is unrelated to the PR's stated DeferredWorkTimer purpose.
Extended reasoning...
What the issue is
Commit 345cdc9 changes src/codegen/bake-codegen.ts:56 from passing css(...) directly as the OVERLAY_CSS define value to passing JSON.stringify(css(...)). After this change, the define value is a JSON string literal (e.g. "\"*{box-sizing:...}\""), not raw CSS starting with *{. Two existing in-tree comments document the old behavior as their motivating example:
src/parsers/json_lexer.rs:1302-1306— explains why*/?/(/)are tokenized in JSON mode without erroring, so the auto-quote fallback can rescue "aBun.builddefine:whose value is a raw minified CSS string starting with*{...}(bake-codegen.ts'sOVERLAY_CSS)".test/bundler/bun-build-api.test.ts:73-76— "a raw minified CSS string starts with*{...}, which src/codegen/bake-codegen.ts passes verbatim asOVERLAY_CSS".
Both are present-tense factual claims about what bake-codegen.ts does, and after this PR both are false.
The code path
Before this PR, bake-codegen.ts ran Bun.build({ define: { OVERLAY_CSS: css(...) } }) where css() returns minified CSS like *{box-sizing:border-box}.... The bundler's define-value parser (parse_env_json → JSONLikeParser) sees a value starting with *, which is not valid JSON. The json_lexer special-case at lines 1297-1309 tokenizes */?/(/) without erroring so that parse_expr's auto-quote fallback can wrap the whole thing as a string literal. The two comments cite OVERLAY_CSS as the concrete in-tree consumer that motivated this special-case.
After this PR, bake-codegen.ts passes JSON.stringify(css(...)) — a value starting with ", which the JSON lexer parses as a string literal directly. The auto-quote fallback is never reached for OVERLAY_CSS.
Why nothing prevents it
This is purely documentation drift. Nothing in the build or test suite checks that explanatory comments stay synchronized with the code they reference. The json_lexer special-case itself remains load-bearing (the test at bun-build-api.test.ts:77-81 covers *{box-sizing:border-box}..., ?foo, (parenthesized), )close — all user-reachable via Bun.build({ define: { X: "*{...}" } })), so removing the code would be wrong; only the comment's "this is why bake-codegen needs it" justification is stale.
Step-by-step proof
css("../runtime/bake/client/overlay.css", ...)returns a string like*{box-sizing:border-box}.root{all:initial}...(minified CSS, starts with*).- Before:
define: { OVERLAY_CSS: "*{box-sizing:...}" }→ bundler parses define value → first char*→ json_lexer hits theTAsteriskarm at ~line 1299 without erroring →parse_exprfalls back to auto-quote → emits string literal. The comment at line 1304 correctly says "bake-codegen.ts's OVERLAY_CSS" is the motivating example. - After (this PR):
define: { OVERLAY_CSS: JSON.stringify("*{box-sizing:...}") }=define: { OVERLAY_CSS: "\"*{box-sizing:...}\"" }→ first char"→ json_lexer parses a string literal directly → auto-quote fallback never invoked. - A future maintainer reads json_lexer.rs:1304, follows the cross-reference to bake-codegen.ts:56, finds
JSON.stringify(...), and concludes the comment is wrong / the special-case might be dead code. It isn't dead (users can still pass raw CSS), but the citation no longer demonstrates it.
Impact
Zero functional impact — this is a documentation-accuracy nit. The risk is purely maintainability: the json_lexer comment is the documented justification for a non-obvious lexer behavior, and a stale justification invites a future "this looks dead, let's remove it" mistake. Per the repo's CLAUDE.md, code changes that invalidate explanatory comments should update them in the same PR.
Also worth flagging: this bake-codegen.ts change is a drive-by fix for an unrelated musl build failure (per the robobun CI comment on commit 93bd662) and has nothing to do with the PR's stated DeferredWorkTimer / FinalizationRegistry purpose. That's fine, but it makes it easy to miss that two comments elsewhere in the tree reference the changed line.
How to fix
Drop or generalize the bake-codegen citation in both places. For example, in json_lexer.rs:1302-1304:
// ... unquoted value that starts with one — e.g. a `Bun.build`
// `define:` whose value is a raw minified CSS string starting
// with `*{...}`. Erroring here aborts ...and in bun-build-api.test.ts:75-76:
// first character — e.g. a raw minified CSS string starts with `*{...}`.Alternatively keep the example but make it past-tense / historical ("bake-codegen.ts used to pass OVERLAY_CSS this way").
There was a problem hiding this comment.
Fair — those comments now describe a historical rather than live example, but the json_lexer auto-quote behavior they document is still correct and useful for any other raw define value. Leaving them as-is to keep this PR scoped to the FinalizationRegistry fix; they can be tightened when that file is next touched.
When a FinalizationRegistry cleanup callback (or any other DeferredWorkTimer task) throws, the exception was leaking out of Bun__runDeferredWork and tripping the caller's assertNoExceptionExceptTermination() in debug/ASAN builds. Match JSC's own DeferredWorkTimer::doWork: wrap the task in a TopExceptionScope and report any non-termination exception via reportUncaughtExceptionAtEventLoop.
TicketData::target() asserts isTargetObject() whose inline definition lives in DeferredWorkTimerInlines.h, so release-ASAN link fails without it. Test: loop gc+setImmediate for reliability across platforms and filter the ASAN signal-handler warning from stderr before asserting empty.
For FinalizationRegistry (or other deferred-work targets) created inside
a node:vm context, target()->realm() is a NodeVMGlobalObject which fails
the inherits(Zig::GlobalObject) check in Bun__handleUncaughtException,
bypassing process.on('uncaughtException'). Normalize to the main global.
The auto-quote fallback for raw CSS in define values (314d044) only works when the codegen-running bun binary already includes that fix. Explicitly quoting the CSS string sidesteps the bootstrap dependency and is semantically equivalent (OVERLAY_CSS is declared as a string).
345cdc9 to
5d967d6
Compare
|
Current CI failures are infrastructure-only and unrelated to this change:
The new regression test passes on every platform that executed it, including linux-x64-asan where the original assertion reproduces. Re-running the failed jobs should clear these. |
When a
FinalizationRegistrycleanup callback (or any otherDeferredWorkTimertask) throws, the exception was leaking out ofBun__runDeferredWork. In debug/ASAN builds this trips the caller'sassertNoExceptionExceptTermination()inJSCDeferredWorkTask::run:Repro
ArrayBufferis callable so it passes theFinalizationRegistryconstructor check, but throws when invoked withoutnew. Any throwing cleanup callback reproduces.Fix
Match JSC's own
DeferredWorkTimer::doWork: wrap the task invocation in aTopExceptionScopeand, if a non-termination exception is pending afterward, clear it and report it viareportUncaughtExceptionAtEventLoop. This routes the error throughprocess.on("uncaughtException")as Node does.Found by Fuzzilli (fingerprint
bfc05fc36c3ce2cf).