Skip to content

worker_threads: carry parse error location (file/line/column/stack) to parent SyntaxError - #34333

Closed
robobun wants to merge 4 commits into
mainfrom
farm/21c13a43/worker-syntax-error-location
Closed

worker_threads: carry parse error location (file/line/column/stack) to parent SyntaxError#34333
robobun wants to merge 4 commits into
mainfrom
farm/21c13a43/worker-syntax-error-location

Conversation

@robobun

@robobun robobun commented Jul 16, 2026

Copy link
Copy Markdown
Collaborator

A worker whose entry file fails to parse reports a SyntaxError with the right name but nothing else: err.stack is undefined, and there is no sourceURL/line/column. 1.3.14 reported name: "Error" (wrong subtype) but carried the full code frame and file path. Node reports both.

// bad.js:  const y = ;
import { Worker } from "node:worker_threads";
const worker = new Worker(new URL("./bad.js", import.meta.url));
const e = await new Promise(r => { worker.on("message", r); worker.on("error", r); });
console.log(JSON.stringify({ name: e?.name, hasStack: typeof e?.stack === "string", keys: Reflect.ownKeys(Object(e)) }));
name keys stack
1.3.14 Error message, cause, originalLine, originalColumn, line, column, sourceURL, stack code frame + at /abs/bad.js:2:11
node v26.3.0 SyntaxError name, message, stack code frame + at /abs/bad.js:2
main SyntaxError message undefined
this PR SyntaxError message, line, column, sourceURL, stack code frame + at /abs/bad.js:2:11

A control worker doing throw new SyntaxError("x") already arrives with ["message","line","column","sourceURL","stack"] on main: structured clone carries these fields fine. The synthesized error simply had none to carry.

Cause

on_unhandled_rejection in src/jsc/web_worker.rs converts a BuildMessage (which doesn't survive structured clone) into a SyntaxError, but built it from only msg.data.text ("Unexpected ;"), dropping msg.data.location (file, line, column, line text). The error is created from native code with no JS frames on the stack, so its lazy stack materialization produces nothing.

Fix

After building the SyntaxError, attach stack (the parser's formatted code frame + at <file>:<line>:<column>, via Msg::write_format) and sourceURL/line/column from msg.data.location as own properties. The structured-clone serializer reads exactly those own fields.

Tests

Strengthened the existing "support worker eval that throws" test, whose assertion had been relaxed from toInclude("error: Unexpected throw") to toInclude("Unexpected throw") when the SyntaxError conversion landed, letting the location loss through. Added a file-based worker test asserting sourceURL/line/column and that stack includes <file>:2:11 and the source line.

Both tests fail on main (stack is undefined, location fields absent) and pass with this change. test/js/node/test/parallel/test-worker-syntax-error{,-file}.js still pass.


[review] gate passed · iteration 2 · 2 files touched

fails on main (without fix)
ASAN without fix: 2 FAILED
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/js/node/worker_threads/worker-syntax-error.test.ts
info: syncing channel updates for nightly-2026-05-06-x86_64-unknown-linux-gnu
info: latest update on 2026-05-06 for version 1.97.0-nightly (e95e73209 2026-05-05)
info: component rust-src is up to date
info: checking for self-update (current version: 1.29.0)
bun test v1.4.0 (9ab2691ff)

test/js/node/worker_threads/worker-syntax-error.test.ts:
18 |     message: result?.message,
19 |     hasStack: typeof result?.stack === "string",
20 |     sourceURL: typeof result?.sourceURL,
21 |     line: result?.line,
22 |     column: result?.column,
23 |   }).toEqual({
          ^
error: expect(received).toEqual(expected)

  {
-   "column": 11,
-   "hasStack": true,
-   "line": 2,
+   "column": undefined,
+   "hasStack": false,
+   "line": undefined,
    "message": "Unexpected ;",
    "name": "SyntaxError",
-   "sourceURL": "string",
+   "sourceURL": "undefined",
  }

- Expected  - 4
+ Received  + 4

      at <anonymous> (/workspace/bun/test/js/node/worker_threads/worker-syntax-error.test.ts:23:
... (truncated)

release without fix: all passed
bun test v1.4.0-canary.1 (447defcbd)

test/js/node/worker_threads/worker-syntax-error.test.ts:
(pass) worker entry-point parse errors surface file path, line and code frame [36.29ms]
(pass) worker eval parse errors surface line, column and code frame [34.52ms]

 2 pass
 0 fail
 11 expect() calls
Ran 2 tests across 1 file. [232.00ms]
__F:0:S:0
passes on PR (with fix)
ASAN with fix: all passed
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/js/node/worker_threads/worker-syntax-error.test.ts
info: syncing channel updates for nightly-2026-05-06-x86_64-unknown-linux-gnu
info: latest update on 2026-05-06 for version 1.97.0-nightly (e95e73209 2026-05-05)
info: component rust-src is up to date
info: checking for self-update (current version: 1.29.0)
bun test v1.4.0 (9ab2691ff)

test/js/node/worker_threads/worker-syntax-error.test.ts:
(pass) worker entry-point parse errors surface file path, line and code frame [1784.05ms]
(pass) worker eval parse errors surface line, column and code frame [1781.59ms]

 2 pass
 0 fail
 11 expect() calls
Ran 2 tests across 1 file. [5.70s]
__F:0:S:0

release with fix: all passed
$ bun scripts/build.ts --profile=release
info: syncing channel updates for nightly-2026-05-06-x86_64-unknown-linux-gnu
info: latest update on 2026-05-06 for version 1.97.0-nightly (e95e73209 2026-05-05)
info: component rust-src is up to date
info: checking for self-update (current version: 1.29.0)
[configured] bun-profile → bun (stripped)
  target       linux-x64-gnu
  build type   Release
  build dir    ./build/release
  revision     9ab2691ff7
  features     (none)

22 deps, 106 codegen, 1168 objects in 787ms

ninja: Entering directory `/workspace/bun/build/release'
[1/91] gen ErrorCode+*.h
[2/48] gen bake.{client,server,error}.js
-> bake.client.js, bake.server.js, bake.error.js
[3/48] gen generated_host_exports.rs
generated_host_exports.rs: 91 exports (host=3, lazy=10, generic=78, rust=0); 243 extern-C blocks audited
[4/48] gen cpp.rs (cppbind)
[5/48] gen ZigGeneratedClasses.{cpp,h,rs}
Found 2 classes from /workspace/bun/src/jsc/resolve_message.classes.ts
  - ResolveMessage (13 fields)
  - BuildMessage (10 fields)
Found 1 classes from /workspace/bun/src/runtime/api/Archive.classes.ts
  - Archive (4 fields, 1 class fields)
Found 2 classes from /workspace/bun/src
... (truncated)
diff hotspot
src/jsc/web_worker.rs                              | 45 ++++++++++++++++--
 .../worker_threads/worker-syntax-error.test.ts     | 53 ++++++++++++++++++++++
 2 files changed, 93 insertions(+), 5 deletions(-)

gate history · 2 passed · 1 rejected · iteration 2

evidence per changed file
file                                                     reads  edits  tests
src/jsc/web_worker.rs                                        3      1      0
test/js/node/worker_threads/worker-syntax-error.test.ts      0      1      0

…o parent SyntaxError

A worker whose entry file fails to parse now surfaces a SyntaxError with
stack, sourceURL, line and column populated from the parser's Location.
Previously on_unhandled_rejection rebuilt the SyntaxError from only
msg.data.text, dropping msg.data.location, so err.stack was undefined
and no file path reached the parent.
@robobun

robobun commented Jul 16, 2026

Copy link
Copy Markdown
Collaborator Author

Superseded by #38324 (closed).

Reproduced with a file-based worker and an eval worker: both yield a SyntaxError with stack === undefined and only ["message"] as own keys on main. After the fix, stack carries the code frame + at <file>:<line>:<column> and sourceURL/line/column are populated.

CI (build 73898): test/js/node/worker_threads/worker-syntax-error.test.ts passes on every lane. Remaining red is unrelated pre-existing main failures, each already owned separately: timer-heap-race.test.ts (JSCTaskScheduler leak, #34293), test-worker-message-port-transfer-terminate.js, test-net-connect-memleak.js, no-orphans.test.ts. The rest passed on retry. Ready for review.

@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 43 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: 39238894-7c30-40f0-a4ed-72b36bcc0418

📥 Commits

Reviewing files that changed from the base of the PR and between 4bbe075 and 447defc.

📒 Files selected for processing (2)
  • src/jsc/web_worker.rs
  • test/js/node/worker_threads/worker-syntax-error.test.ts

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

@robobun

robobun commented Jul 16, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 7:50 AM PT - Jul 16th, 2026

@robobun, your commit 9ab2691 has 4 failures in Build #73898 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 34333

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

bun-34333 --bun

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

LGTM — small, well-scoped restoration of parse-error location info that a prior refactor dropped.

Checked: Msg::clone() is an inherent method (src/ast/lib.rs:1233); write_format::<false> signature matches; Location.line/column guard values (> 0 / > -1) match the field docs (1-based line, 0-based column). JSValue::putputDirect (bindings.cpp:3838), so no user JS runs and no exception-check needed on a fresh SyntaxError. ZigString::from_bytes(...).to_js() is the established pattern (e.g. filesystem_router.rs:62). Tests cover both eval and file-based workers and assert exact line/column/sourceURL plus code-frame content.

Extended reasoning...

Overview

The PR touches one function (on_unhandled_rejection in src/jsc/web_worker.rs) and one test file. When a worker's entry point fails to parse, the resulting BuildMessage is converted to a SyntaxError before structured-cloning to the parent. Previously only msg.data.text was carried; this change clones the whole Msg, formats the parser's code frame via Msg::write_format::<false> into stack, and attaches sourceURL/line/column from msg.data.location as own properties — the exact fields the structured-clone Error serializer reads. Per the PR table, this restores what 1.3.14 already reported (plus the correct SyntaxError subtype).

Security risks

None. This is error-metadata surfacing on an internal worker→parent path. No user-controlled input reaches new parsing/allocation logic; the file path in sourceURL was already user-visible in 1.3.14 and in Node.

Level of scrutiny

Low-to-moderate. ~35 net lines in a single error-handling block, no new control flow, no cross-thread state, no allocator/lifetime changes. Verified:

  • BuildMessage.msg is bun_ast::Msg (src/jsc/BuildMessage.rs:14) with an inherent clone() (src/ast/lib.rs:1233).
  • Location { file: Cow<[u8]>, line: i32, column: i32 } with docs "Line <= 0 means no line/column info" and column "0-based" — the line > 0 / column > -1 guards are correct.
  • JSC__JSValue__put calls putDirect (src/jsc/bindings/bindings.cpp:3838), so no JS reentry / exception-check concern on a freshly created error instance.
  • ZigString::from_bytes(...).to_js(global) is used identically elsewhere (filesystem_router.rs:62); bytes are copied into the JS heap before the owning String/Cow drops.

Other factors

Test coverage is solid: the existing eval-worker test is strengthened back to assert the full "error: ..." prefix and at <file>:1:13, and a new file-based test asserts exact line/column/sourceURL and that stack contains both the file:line:col and the source line. Both are stated to fail on main and the vendored test-worker-syntax-error{,-file}.js still pass. No prior reviewer comments to address; CI is building.

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

LGTM — focused restoration of parse-error location on the worker→parent SyntaxError path.

Checked: ZigString::from_bytes auto-tags UTF-8 for non-ASCII paths/source lines; .put() targets a freshly-created native error (no user setters, no exception-check needed); location.line > 0 / column > -1 match the Location field docs (1-based line, 0-based column, ≤0 = no info); write_format::<false> into String cannot fail so discarding the result is fine. Tests cover both file and eval workers and fail on main.

Extended reasoning...

Overview

Extends the existing BuildMessageSyntaxError conversion in on_unhandled_rejection (src/jsc/web_worker.rs) to attach stack (via Msg::write_format::<false>) and sourceURL/line/column from msg.data.location as own properties on the synthesized error, so structured clone carries them to the parent. The only unsafe change is widening the existing (*bm).msg.data.text.clone() to (*bm).msg.clone() under the same SAFETY contract. New test file exercises file-based and eval-based worker parse errors.

Security risks

None. This is an error-reporting path; inputs are the parser's own Msg/Location data, not user-controlled beyond what was already displayed in the message. No auth, crypto, filesystem, or network surface touched.

Level of scrutiny

Moderate — web_worker.rs is threading- and GC-sensitive, but this change is confined to a single already-existing conversion block on the worker (JS) thread inside on_unhandled_rejection. The target of the .put() calls is a native-created SyntaxError (no proxy/setter re-entry), the string data is Rust-owned and lives across the to_js call, and ZigString::from_bytes handles the ASCII/UTF-8 tag automatically. No new locking, no cross-thread state, no new lifetime edges.

Other factors

The PR includes robobun evidence showing both tests fail on main (stack undefined, location fields absent) and pass with the fix under debug+ASAN and release. The finder-raised encoding concern was verified against ZigString::from_bytes (auto-tags UTF-8 on non-ASCII). No CODEOWNERS match for the touched paths and no outstanding human review comments.

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

LGTM — focused fix that attaches parser location (stack/sourceURL/line/column) to the synthesized SyntaxError before it's structured-cloned to the parent.

Checked: Msg::clone() deep-dupes the Cow-borrowed file/line_text so the cloned location outlives the BuildMessage cell; the line > 0 / column > -1 guards match the documented Location sentinel semantics in src/ast/lib.rs.
Checked: .put() on a fresh native SyntaxError instance runs no user JS, so no new exception-check obligations vs. the pre-existing path.
Checked: pattern matches BuildMessage.rs:148-161 (ZigStringto_js, put for location fields).
Tests use tempDir, cover file + eval workers, and the PR evidence shows fails-on-main / passes-with-fix under ASAN.

Extended reasoning...

Overview

The PR touches on_unhandled_rejection in src/jsc/web_worker.rs (~40 net lines) and adds a new 53-line test file. When a worker's entry point fails to parse, the existing code already converted the resulting BuildMessage into a SyntaxError (because BuildMessage doesn't survive structured clone), but only carried msg.data.text — dropping the file/line/column/code-frame that 1.3.14 used to report. This change clones the full Msg, formats it via the existing Msg::write_format::<false> into a stack string, and attaches sourceURL/line/column from msg.data.location as own properties on the error before it's serialized to the parent.

Security risks

None. This is error-reporting metadata on a developer-facing SyntaxError. No auth, crypto, permissions, or untrusted-input parsing paths are touched. The added .put() calls target a freshly-created native error object (no proxy traps / user setters), so no new re-entrancy surface.

Level of scrutiny

Low-to-medium. The change is additive to an existing error-conversion branch and uses well-established helpers verified against neighboring code:

  • Msg has an inherent clone() (src/ast/lib.rs:1233) that deep-copies Location's Cow<'static, [u8]> fields, so the cloned data is safe past the BuildMessage cell's lifetime.
  • The location.line > 0 / location.column > -1 guards match the field docs at src/ast/lib.rs:726-733 (line ≤ 0 = no location; column is 0-based).
  • ZigString::from_bytes(...).to_js() + JSValue::put() mirrors the exact pattern in BuildMessage.rs:148-161 for the same Location fields.
  • write_format::<false> writes to impl fmt::Write (String here) and its fmt::Result is intentionally discarded (String writes never fail).

Other factors

  • Tests are placed in a dedicated file per commit da5c8c9 (avoiding pre-existing debug+ASAN timeouts in worker_threads.test.ts), use tempDir from harness, assert exact values, and normalize path separators for Windows.
  • PR evidence shows both tests fail on main and pass on the PR under debug+ASAN and release; test-worker-syntax-error{,-file}.js node-parallel tests still pass.
  • Verifiers ruled out: worker cleanup ordering (worker self-exits after parse failure, so terminate() after assertions is a no-op); comment length (added comments are exactly 3 lines).
  • No CODEOWNERS entries match the touched paths; no outstanding reviewer comments; no prior claude[bot] reviews on this PR.

@robobun

robobun commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator Author

Closing in favour of #38324, which carries the same fix (the parse error's sourceURL, line, column and stack reach the parent's SyntaxError) and also makes BuildMessage and ResolveMessage structured-cloneable, so the worker-only conversion this PR patched is removed there rather than extended.

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