Skip to content

worker: fill the error event's filename, lineno and colno - #45429

Open
robobun wants to merge 3 commits into
mainfrom
robobun/592381ed/worker-error-event-fields
Open

robobun wants to merge 3 commits into
mainfrom
robobun/592381ed/worker-error-event-fields

Conversation

@robobun

@robobun robobun commented Oct 15, 2026 •

Copy link
Copy Markdown
Collaborator

Part of #23473

Problem

Fix

  • on_unhandled_rejection passes the JSC::Exception on. WebWorker__dispatchError fills the fields with the new scriptPositionOfThrownValue (JSDOMExceptionHandling.cpp).
  • The position is the first frame of the program on the throw's stack. Without a JSC::Exception it is the position that a native Error or DOMException carries.
  • message does not change. error is the thrown value, also when its console rendering throws.
  • Verified: test/js/web/workers/worker-error-event.test.ts (32 of 36 fail before). Self-reviewed: 45 concerns raised, 2 left (Notes).

Background

  • HTML leaves the three fields to the implementation. WPT expects the script URL and the throw's line.
  • A JSC::Exception holds a thrown value and the stack at the throw. Timers and listeners pass it to the hook. A rejected promise or a failed entry passes only the value.
  • A frame is the program's when its code is neither a JavaScriptCore builtin nor a module built into Bun (Zig::isScriptFrame).
  • Considered: a one-line message, as in browsers. A listener's message is today the only place that shows the stack, so that waits for worker: report a Worker error that no listener took as the parent's uncaught error #35829.

Downsides

  • Without its JSC::Exception, a top-level throw 5 or [].reduce(cb) still reports "", 0, 0.
  • A worker error makes 0 to 10 more allocator calls (39 to 43 for a top-level Error).
  • Release .text grows 3,072 bytes.
Notes

What the two events say. The worker's own event and the Worker object's event carry the same filename, lineno, colno.

uncaught value position
an Error thrown at the top level, an unhandled rejection, reportError(error) where the Error was made
any value thrown in a timer, an event listener, onmessage, a microtask, nextTick where it was thrown
an Error that a builtin or a module built into Bun threw under such a callback ([].reduce(cb), emitter.emit("error", x), new WebSocket("x") from ws) the script's call
a module that does not parse, required under such a callback the script's require()
an Error out of structuredClone() where the first Error was made
a DOMException that native code threw (structuredClone(() => {})) the script's call
a SyntaxError that only JavaScriptCore's parser finds (a bad regular expression) the line in the source, colno 0
a number, a script-made DOMException, an object that is not a native Error, each thrown at the top level "", 0, 0
an Error that a builtin or a module built into Bun threw at the top level "", 0, 0
a parse error or a module that does not resolve "", 0, 0 (as before)
  • filename names the frame as stack does. An absolute path is written as a file: URL, so a module on disk gives its import.meta.url. A blob: or data: worker gives that URL. The query and the fragment of the URL given to new Worker are not part of it.
  • The position is source-mapped, as in stack: a .ts worker reports the line of its source. In a bun build --compile executable the frame is in the bundle (file:///$bunfs/root/worker.js). With --sourcemap it is the path that the map names (src/lib/thrower.ts), which is not a URL. An Error whose stack was read keeps only that name, so the event gives it in both states.
  • The column is the one stack gives that frame. Chrome reports the start of the statement.
  • A call in tail position leaves no frame (every strict function, so every ES module): return JSON.parse(text) reports the caller of that function.
  • The function runs no script. It reads frames, the cell's line and column, and own data properties.

Why an Error that starts inside the runtime says nothing without its JSC::Exception. When stack is read, or when a collection finds one of the frames dead, JavaScriptCore replaces the frames with the position of the first frame that has one. test/js/bun/jsc/webkit-upgrade-7b485a76e9.test.ts pins that line and column name that frame, also when it is a builtin. The event reports that frame or nothing, so that the same Error gives the same answer before and after. An earlier draft of this PR changed which frame is stamped. That is a change for every program and is not in this PR.

Not changed.

An abort that goes away. When the console printer threw (an inspect hook that throws), the hook replaced the event's error with what the printer threw, as a JSC::Exception cell. A listener in the worker that touched it ended the process (exit 134). The thrown value now stays the event's error, and the position comes from what was thrown. A node:worker_threads parent then gets the clone of that value, where it got null. The test a value whose console rendering throws covers it. #38560 has the same change in on_unhandled_rejection. A typed take_exception would fix the class and is not in this PR.

Related pull requests. This PR does not depend on any of them.

Self-review. The review asked for this narrower cut: no change to which frame stack stamps, no second mapping for parse errors, message as on main, builtin code detected from the code block and not from a URL prefix. Two things are left.

  • console.log(event) prints type, message and error, and not the three new fields. The location is still in message, so that goes with the message change.
  • The web platform tests are not run through test/js/third_party/wpt-testharness-shim.ts. The test file has workers/support/ErrorEvent.js unchanged, the checks of Worker_ErrorEvent_filename.htm, _lineno.htm and _message.htm, and a test.todo for WorkerGlobalScope_ErrorEvent_*.htm.

Left for later.

  • The test.todo rows in the test file: a position for a value that carries none and arrives without its JSC::Exception, and self.onerror called with five arguments. It gets the event alone.
  • node:fs, node:net and Bun.serve callbacks do not pass the JSC::Exception to the hook. A throw 5 there, or if (err) throw err with an Error that the runtime made, reports "", 0, 0. An Error made in its throw statement reports its line.
  • An Error whose stack was read under a user Error.prepareStackTrace has no stamped position.
  • WebCore::reportException still reports "", 0, 0. It can use the same function.

Measurements. Release builds of main 116dd64 and of this branch at cff528d. gdb on bun-profile, JIT off, three runs with identical counts. strace, valgrind, perf and bloaty are not available in the container.

  • release .text: +3,072 bytes (size -A)
  • allocator calls inside the error hook per worker error: Web Error at the top level 39 -> 43, Web Error from a timer 32 -> 36, Web throw 5 from a timer 8 -> 18, Web script-made DOMException 37 -> 37, Node cloneable Error 50 -> 52, Node throw port1 105 -> 105 (counters on 18 mimalloc entry points, GC cells not counted)
  • error task closure: 16 -> 32 bytes, tasks posted per error 1 -> 1 (static_assert on the lambda, hit counts on postTaskTo)
  • clean worker: error-hook entries 0 -> 0, flush_logs the same 115 instructions, formatStackTrace the same 7,120 bytes
  • computeLineColumnWithSourcemap (CPU profilers, v8::Function positions): its body moved to remapLineColumn. The hook is now 10 instructions and a tail call, and no longer takes and drops a reference on the URL (11 instructions, two of them atomic).

Suites run on the debug build of this branch.

  • worker-error-event.test.ts: 36 pass, 2 skip, 5 todo. Also with BUN_JSC_validateExceptionChecks=1. The 2 skipped tests build an executable. They pass on the release build (38 pass). On a release build of main, 34 of the 38 fail.
  • test/js/web/workers: worker.test.ts 42, worker_blob 6, worker-entry-point 2, message-event 10, structured-clone 244.
  • test/js/node/worker_threads/worker_threads.test.ts: 153 pass in three of four runs. One run failed terminate(): nothing of the worker's runs after the request, which passes alone six times of six.
  • test/cli/run/cpu-prof.test.ts 12 (it uses the source-map hook).
  • Stack and position: webkit-upgrade-7b485a76e9 22, webkit-upgrade-9b02218df6 4, capture-stack-trace 68, error-gc-test 4, inspect-error 39, inspect.test.js 96, reportError 2, internal-sourcemap 6, internal-sourcemap-roundtrip 22, vm.test.ts 309, console-log 5.
  • Not run: macOS and Windows.

Questions for a maintainer.

The 'error' event of a worker had an empty filename and 0 for lineno and
colno, in the worker and on the Worker object. Take the three from the
throw's stack when the JSC::Exception is still there, else from the
position the thrown Error or DOMException carries.
@robobun

robobun commented Oct 15, 2026 •

Copy link
Copy Markdown
Collaborator Author

Status

How I reproduced it: the script from #32342 on a release build of main (116dd64) prints filename: "", lineno: 0, colno: 0 for the error event on the Worker object. With this branch it prints filename: "data:,throw new Error('foo')", lineno: 1, colno: 11. message is the same on both.

test/js/web/workers/worker-error-event.test.ts fails 34 of its 38 tests on that build of main and passes on this branch. The other 4 tests pin cases that report no position.

Pull request: #45429

@coderabbitai

coderabbitai Bot commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository: oven-sh/bun/.coderabbit.yaml
  • Review profile: ASSERTIVE
  • Plan: Advanced
  • Run ID: 91adca33-a4fc-4d6d-9ff2-0ea0213f38cf
📥 Commits

Reviewing files that changed from the base of the PR and between cff528d and 2f7d6e7.

📒 Files selected for processing: 7
  • src/jsc/bindings/ErrorStackTrace.cpp
  • src/jsc/bindings/ErrorStackTrace.h
  • src/jsc/bindings/FormatStackTraceForJS.h
  • src/jsc/bindings/JSDOMExceptionHandling.cpp
  • src/jsc/bindings/JSDOMExceptionHandling.h
  • src/jsc/bindings/webcore/Worker.cpp
  • src/jsc/web_worker.rs

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 7 remain after this review.

Walkthrough

Worker error reporting derives script positions from thrown values and exception stacks, then includes filename, line, and column in worker error events. The change adds script-frame and source-URL classification, source-map remapping support, and tests for error paths and URL forms.

Changes

Worker Error Event Locations

Layer / File(s) Summary
Script source classification and remapping
src/jsc/bindings/ErrorStackTrace.*, src/jsc/bindings/InternalModuleRegistry.*, src/jsc/bindings/FormatStackTraceForJS.*
Adds checks for script frames, generated URLs, and internal module names. Adds a helper that remaps line and column values for a source URL.
Position extraction from thrown values
src/jsc/bindings/JSDOMExceptionHandling.*
Adds ScriptPosition and scriptPositionOfThrownValue. Position extraction checks exception frames and supported error properties, and remaps source positions where applicable.
Worker error location delivery
src/jsc/bindings/webcore/Worker.*, src/jsc/bindings/webcore/WorkerMessagingProxy.*, src/jsc/web_worker.rs, test/js/web/workers/worker-error-event.test.ts
Worker error dispatch passes the thrown value and derived location through the messaging proxy. Tests cover error types, execution paths, URL forms, source maps, and error-value handling.

Suggested reviewers: dylan-conway

Priority: ➖ Normal

Merge Risk: ⚪ Minimal · up to 2f7d6

Worker error events preserve the intended error values and report locations using the existing coordinate convention; no merge-blocking behavior is established.

Pre-merge checks: Passed 4
Check name Status Explanation
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.
Title check Passed The title clearly and concisely describes the main change: populating worker error events with filename, line number, and column number.
Description check Passed The description clearly explains the problem, implementation, scope, limitations, related work, and verification results. It does not use the exact template headings, but it provides the required info…
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@robobun

robobun commented Oct 15, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 7:05 PM PT - Oct 15th, 2026

❌ @robobun, your commit 2f7d6e7 has some failures in Build #126291 (All Failures)


🧪   To try this PR locally:

bunx bun-pr 45429

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

bun-45429 --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.

Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.

Beyond the inline findings, I also checked: the remapLineColumn extraction keeps the empty-URL guard in computeLineColumnWithSourcemap and every new direct caller is gated by isScriptFrame/isScriptSourceURL, so no behavior change for existing .stack formatting; the new filename is isolatedCopy()'d into the cross-thread task and both the Web and Node postErrorToWorkerObject arms pass the three fields; scriptPositionOfThrownValue only uses getDirect/tryGetValue/cell accessors, so it cannot re-enter JS from the error hook.

Extended reasoning...

The change adds position extraction for worker uncaught errors across the C++ error-hook path (JSDOMExceptionHandling, Worker.cpp, WorkerMessagingProxy), a stack-frame predicate in ErrorStackTrace, and a Rust change passing the JSC::Exception through; no security-sensitive surface. Inline findings already cover the compiled-sourcemap filename shape, the BuildMessage path dropping the exception, and the inspect-hook error substitution, so this note only records the thread-safety, refactor-equivalence and re-entrancy checks that were ruled out.

Additional findings (outside the current diff — GitHub can't attach inline comments there):

  • 🟡 src/jsc/web_worker.rs — A worker whose callback does require("./file-with-syntax-error.js") inside a timer, listener or onmessage still gets filename "", lineno 0, colno 0, even though the JSC::Exception carrying the script's require call site arrived. src/jsc/web_worker.rs:1163-1167 replaces the BuildMessage value with a synthetic SyntaxError, so error_instance != thrown_value at web_worker.rs:1210 and the original exception is dropped. The synthetic error has no script frames. Fix: pass the original JSC::Exception for the position while still dispatching the synthetic SyntaxError as the event's error, e.g. by splitting the position lookup from the value in WebWorker__dispatchError.

    Why this was flagged

    A worker's setTimeout or onmessage handler requires a module with a parse error, or imports one whose resolution fails. The hook receives a JSC::Exception whose value is a BuildMessage (web_worker.rs:1142). Line 1163 matches it and line 1167 builds a fresh SyntaxError via to_syntax_error_instance, created from native code with no JS frames. At web_worker.rs:1210 error_instance == thrown_value is false, so thrown is the synthetic error and the JSC::Exception with the script's frame is discarded. scriptPositionOfThrownValue at src/jsc/bindings/JSDOMExceptionHandling.cpp:146 gets exception == nullptr; the ErrorInstance arm finds line()==0 and either no frames or a native-only frame and returns {}. The PR's own table promises 'where it was thrown' for any value thrown in a timer or listener; this input gets "",0,0 instead. The base also reported "",0,0, but the fix is one line: pass error_instance_or_exception for the position and the synthetic error for init.error.

    Verification: nit (pre-existing route: the base also reports "", 0, 0 here). Trigger: a worker callback under a timer/listener/onmessage calls require() on a file with a parse error. src/jsc/web_worker.rs:1163-1167 replaces error_instance with a SyntaxError, so line 1210 if error_instance == thrown_value is false and the JSC::Exception is dropped, giving filename "", lineno 0, colno 0.

Comment thread src/jsc/bindings/webcore/Worker.cpp
Comment thread test/js/web/workers/worker-error-event.test.ts Outdated
Comment thread src/jsc/web_worker.rs Outdated
… throws

The position now comes from what was thrown, also when the event's error
is a SyntaxError made for a module that did not parse. The test cases run
concurrently.
Comment thread src/jsc/bindings/ErrorStackTrace.cpp Outdated
Comment thread src/jsc/bindings/ErrorStackTrace.h Outdated
Comment thread src/jsc/bindings/ErrorStackTrace.h Outdated
Comment thread src/jsc/bindings/FormatStackTraceForJS.h Outdated
Comment thread src/jsc/bindings/JSDOMExceptionHandling.cpp Outdated
Comment thread src/jsc/bindings/JSDOMExceptionHandling.cpp Outdated
Comment thread src/jsc/bindings/JSDOMExceptionHandling.cpp Outdated
Comment thread src/jsc/bindings/JSDOMExceptionHandling.h Outdated
Comment thread src/jsc/bindings/JSDOMExceptionHandling.h Outdated
Comment thread src/jsc/bindings/webcore/Worker.cpp Outdated
Comment thread src/jsc/web_worker.rs Outdated

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

Code review completed

Nothing new to post: everything this review found is already covered by existing comments on this pull request or didn't merit a separate one.

This branch has not been deployed

No deployments
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.

2 participants