Skip to content

fix(browser): forward uncaught worker errors with their stack - #24210

Open
d2anamaria wants to merge 10 commits into
developfrom
ana/fix/wasm/worker-uncaught
Open

fix(browser): forward uncaught worker errors with their stack#24210
d2anamaria wants to merge 10 commits into
developfrom
ana/fix/wasm/worker-uncaught

Conversation

@d2anamaria

@d2anamaria d2anamaria commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Uncaught errors thrown inside a web worker reach Sentry without a usable stack. They bubble to the page, so an event is still created, but the propagated ErrorEvent carries no error object, only a message string. The result is an event with a single synthetic frame pointing at the worker bundle and a value prefixed with Uncaught .

For plain JavaScript that degrades acceptably: one frame plus a sourcemap still locates the throw. For WebAssembly it fails outright. No frame carries a wasm URL, so the wasm integration finds nothing to match and the event ships with no debug images, even though the worker's images already reached the page. An uncaught wasm trap in a worker is unsymbolicatable today, while the identical trap wrapped in try/catch symbolicates fine.

Root cause

registerWebWorker only forwarded unhandled rejections, on the assumption that synchronous errors were already covered by the global handlers. They are captured, but only from the message string, because an error that crosses a worker boundary loses its error object by design.

Solution

Uncaught worker errors are now forwarded to the page over the same channel that already carries rejections. Structured clone preserves message, stack and cause, so the page receives a real error and parses a real stack, the same outcome the caught path already produced. Wasm frames then match their debug images and symbolicate normally. Forwarded errors are distinguishable from rejections by their mechanism, and the worker's stack trace limit now matches the page's so deep stacks are no longer truncated before being sent.

Structured clone resets any error name outside the built-in set to Error, which would turn a wasm RuntimeError or a custom subclass into a plain Error. The worker sends the name separately and the page restores it before building the event.

The throw still bubbles to the page after the worker forwards it. The page side of the integration already holds the Worker object, so it listens for its error event and tells the global handlers to skip the message-only copy. The event is not cancelled, so the browser still prints its own uncaught report in DevTools. The skip only applies once the worker has announced that it forwards errors, so workers registered by an older SDK version keep the bubbled event and mixed-version setups keep working.

When the error value cannot be structured-cloned, for example a WebAssembly.Exception or an error whose cause holds a function, the worker retries with a fresh error that keeps the message and stack but drops the cause. Values that are not errors go through normalize instead.

Errors that arrive without an error object, such as cross-origin script errors, get a frame from the ErrorEvent location, the same way the global handlers do.

Limitations

A worker on this version paired with a page bundle on an older version still forwards, but the older page ignores the new fields and labels every forwarded error as an unhandled rejection.

- Add an `error` listener in `registerWebWorker` that posts `event.error`
  (falling back to `event.message`) over the existing `_sentryWorkerError` channel
- Add optional `kind` discriminator to `SerializedWorkerError`; a missing `kind`
  means rejection, so workers registered by an older SDK keep working
- Rename `handleForwardedWorkerRejection` to `handleForwardedWorkerError` and
  branch on `kind` for both the mechanism and `eventFromUnknownInput`'s
  `isUnhandledRejection` argument
- Report forwarded throws under the `auto.browser.web_worker.onerror` mechanism
- Restrict `_eventFromRejectionWithPrimitive` to rejections so a thrown primitive
  is not labelled "Non-Error promise rejection"
- Set `Error.stackTraceLimit = 50` in the worker, matching globalHandlersIntegration,
  since V8's default of 10 truncates stacks before they are forwarded
- Wrap the forwarding `postMessage` so a non-cloneable reason is described instead
  of raising DataCloneError out of the worker's error handler
- Correct the doc comment claiming globalHandlers already captures sync worker errors
@d2anamaria
d2anamaria requested a review from a team as a code owner September 8, 2026 14:45
@d2anamaria
d2anamaria requested review from andreiborza, logaretm and msonnb and removed request for a team September 8, 2026 14:45

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

Stale Bugbot comment from a previous run.

Comment thread packages/browser/src/integrations/webWorker.ts Outdated
@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

size-limit report 📦

Path Size % Change Change
@sentry/browser 28.96 kB - -
@sentry/browser - with treeshaking flags 27.26 kB - -
@sentry/browser - with treeshaking flags tracing without tracing 27.15 kB - -
@sentry/browser (incl. Tracing) 50.48 kB - -
@sentry/browser (incl. Tracing + Span Streaming) 50.48 kB - -
@sentry/browser (incl. Tracing, Profiling) 53.47 kB - -
@sentry/browser (incl. Tracing, Replay) 90.02 kB - -
@sentry/browser (incl. Tracing, Replay) - with treeshaking flags 79.11 kB - -
@sentry/browser (incl. Tracing, Replay with Canvas) 94.72 kB - -
@sentry/browser (incl. Tracing, Replay, Feedback) 107.69 kB - -
@sentry/browser (incl. Feedback) 46.46 kB - -
@sentry/browser (incl. sendFeedback) 34.01 kB - -
@sentry/browser (incl. FeedbackAsync) 39.12 kB - -
@sentry/browser (incl. Metrics) 29.98 kB - -
@sentry/browser (incl. Logs) 30.24 kB - -
@sentry/browser (incl. Metrics & Logs) 30.91 kB - -
@sentry/react 30.72 kB - -
@sentry/react (incl. Tracing) 52.78 kB - -
@sentry/vue 36.2 kB - -
@sentry/vue (incl. Tracing) 52.71 kB - -
@sentry/svelte 28.98 kB - -
CDN Bundle 30.7 kB - -
CDN Bundle (incl. Tracing) 50.97 kB - -
CDN Bundle (incl. Logs, Metrics) 32.98 kB - -
CDN Bundle (incl. Tracing, Logs, Metrics) 52.95 kB - -
CDN Bundle (incl. Replay, Logs, Metrics) 73.67 kB - -
CDN Bundle (incl. Tracing, Replay) 88.5 kB - -
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) 90.48 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback) 94.58 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) 96.57 kB - -
CDN Bundle - uncompressed 90.83 kB - -
CDN Bundle (incl. Tracing) - uncompressed 152.26 kB - -
CDN Bundle (incl. Logs, Metrics) - uncompressed 97.41 kB - -
CDN Bundle (incl. Tracing, Logs, Metrics) - uncompressed 158.22 kB - -
CDN Bundle (incl. Replay, Logs, Metrics) - uncompressed 226.82 kB - -
CDN Bundle (incl. Tracing, Replay) - uncompressed 271.83 kB - -
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) - uncompressed 277.78 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed 285.53 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) - uncompressed 291.47 kB - -
@sentry/nextjs (client) 55.09 kB - -
@sentry/sveltekit (client) 50.9 kB - -
@sentry/core/server 37.13 kB - -
@sentry/core/browser 13.66 kB - -
@sentry/node 129.82 kB +0.02% +22 B 🔺
@sentry/node/import (ESM hook with diagnostics-channel injection) 81.83 kB - -
@sentry/node - without tracing 89.62 kB +0.04% +33 B 🔺
@sentry/node - without channel injection 108.7 kB +0.03% +22 B 🔺
@sentry/aws-serverless 97.83 kB +0.02% +18 B 🔺
@sentry/cloudflare (withSentry) - minified 203.36 kB - -
@sentry/cloudflare (withSentry) 506.44 kB - -

View base workflow run

@d2anamaria
d2anamaria requested a review from chargome September 9, 2026 07:19
@github-actions

Copy link
Copy Markdown
Contributor

👋 @logaretm, @msonnb, @andreiborza — Please review this PR when you get a chance!

The parent already holds the Worker object, so it listens for its error
event and tells globalHandlers to skip the frameless copy that bubbles to
window.onerror. The event is not cancelled, so the browser still prints
its own report. The skip only applies once the worker announced that it
forwards errors, so workers on an older SDK keep the bubbled event.

Structured clone resets any error name outside the built-in set, so the
worker sends the name separately and the parent restores it. When the
reason cannot be cloned, the worker retries with a fresh Error that keeps
message and stack, or with a normalized value for anything else.

Message-only errors get a frame from the ErrorEvent location, the same
way globalHandlers does.
cursor[bot]

This comment was marked as outdated.

The ErrorEvent filename can differ from the worker script when the
error comes from an imported module, so the worker forwards it and the
page builds the fallback frame from it.

The single-event e2e test now waits for a second worker's event instead
of sleeping. The bubbled copy of the first throw is queued right behind
the forwarded one, so it would arrive before that event.
cursor[bot]

This comment was marked as outdated.

An ErrorEvent reports an unknown script as an empty string, which
skipped the worker script and left the fallback frame on the page URL.

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 9f65b27. Configure here.

Comment thread packages/browser/src/integrations/webWorker.ts
The page suppresses the bubbled copy of every error once the worker has
announced itself, so a retry that posts another Error loses the error
completely in browsers that cannot clone Error at all. The retry now
sends a plain message and stack copy and the page rebuilds the Error.
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.

2 participants