Premise falsified, 2026-09-23, closed as not planned (author: @neo-opus-ada). This ticket claimed a SharedWorker that fails to load "reaches nobody". That claim was adopted from onWorkerError's comment ("the ONLY signal…") without being tested. Chrome 152 reports the failure in the page console for both kinds of worker: Failed to fetch a worker script. for a SharedWorker 404, for a SharedWorker module parse error, and for a dedicated-worker 404. The measurement below (the port binding is dead, and error fires on the worker object) still holds, but it fixes nothing observable. Rebinding would only duplicate the browser's own line. The crash slice's programmatic start-failure signal moves to #19103. Detail: the [MERGE_HOLD] on PR #19104 (comment 5794279240).
Context
Found on 2026-09-23 while answering a design question on D#18730 (DC 18564414). The v13.2 crash slice needs to know when a window group's canvas SharedWorker fails to start. Today no SharedWorker can report that.
The Problem
Neo.worker.Manager#createWorker (src/worker/Manager.mjs) binds its error handler like this:
(isShared ? worker.port : worker).onerror = me.onWorkerError.bind(me);
For a dedicated Worker that is right: error fires on the worker. For a SharedWorker it is not. A load or parse failure fires error on the SharedWorker object, while worker.port is a MessagePort, which has no error event. Assigning port.onerror creates a property that nothing ever calls.
Measured in Chromium 152, with a SharedWorker whose script returns 404:
| handler |
times fired |
sharedWorker.onerror |
1 |
sharedWorker.port.onerror |
0 |
'onerror' in MessagePort.prototype is false. The control, a SharedWorker that loads, exchanged a message and fired no error.
So onWorkerError never runs for a SharedWorker, although its own comment (written in #18567) calls it "the ONLY signal for a SharedWorker that fails to load or parse". The binding moved onto the port in 2020 (9a720bd634, which switched onmessage onto the port to avoid calling port.start()), and onerror moved with it. #18566's AC-6 later rewrote the handler's body but not where it is bound.
The Architectural Reality
createWorker creates every worker: App, Data, VDom, Canvas and Task. A worker is shared when sharedWorkersEnabled && Neo.config.useSharedWorkers.
onmessage on worker.port is correct and must stay: messages do arrive on the port.
- Only the binding changes. What
onWorkerError logs is not part of this issue.
The Fix
In createWorker, bind onerror on the worker object in both modes:
(isShared ? worker.port : worker).onmessage = me.onWorkerMessage.bind(me);
worker.onerror = me.onWorkerError.bind(me);
The Test
Add test/playwright/unit/worker/ManagerWorkerError.spec.mjs. Neo.worker.Manager reads window when its module loads, so its specs run it in a child process with a stubbed DOM. Copy the harness from test/playwright/unit/worker/ManagerReplySettlement.spec.mjs (runReplyProbe). In the child script:
- stub
globalThis.Worker and globalThis.SharedWorker with a fake class whose instances have port = {} and a method that fires a load error the way the browser does: this.onerror?.({type: 'error'});
- before calling
createWorker, replace WorkerManager.onWorkerError with a function that counts its calls;
- set
WorkerManager.sharedWorkersEnabled and Neo.config.useSharedWorkers for the arm;
- call
WorkerManager.createWorker({fileName: 'App.mjs', basePath: '/'}), fire the error, and print the count.
Two arms:
- SharedWorker: the count is
1. On today's dev it is 0: this exact harness was run before filing, and it printed {"onWorkerErrorCalls":0,"boundOnWorkerObject":false,"boundOnPort":true}.
- Dedicated worker (control): the count is
1 before and after the fix. It proves the harness can see a call at all.
How to run it
npm install
npm run bundle-browser-deps
npm run test-unit -- test/playwright/unit/worker/ManagerWorkerError.spec.mjs
bundle-browser-deps matters: without it the unit tier selects zero tests instead of failing one. Do not run a bare npx playwright test, because this repository has several Playwright configurations.
Acceptance Criteria
Out of Scope
- What
onWorkerError logs, and at which level.
- Errors thrown inside a SharedWorker after it has loaded. Those are mirrored by
worker.Base#forwardErrorToMainThread (#18566).
- Telling the app worker that a canvas worker failed to start. That belongs to D#18730's slice, which depends on this fix.
Related
- #18566 and #18567: rewrote
onWorkerError's body but not its binding. This is their successor; the closed ticket is not reopened.
- D#18730 (DC 18564414): the crash slice's start-failure outcome needs this signal.
- #18985: the Hacktoberfest door this issue is curated for.
Sweeps
- Live latest-open sweep: the latest 20 open issues at 2026-09-23T09:03:08Z; no equivalent.
- A2A in-flight sweep: the last 30 messages in all read states; no claim on this scope.
- MC sweep:
query_raw_memories("SharedWorker fails to load silently no error reported onWorkerError worker.port onerror Manager createWorker"), 6 results, no prior decision found.
- Own-assignment sweep: 5 open issues (#19047, #18985, #16217, #14790, #13015), none on
src/worker.
- Corpus: a search for
onWorkerError finds only #18566 (closed, the predecessor above).
unowned-rationale: a contained first issue with a red-first test already proven, left for a contributor. If nobody has claimed it by 2026-09-26, a maintainer takes it, because D#18730's slice depends on it.
Decision Record impact: none
Origin Session ID: 3be453e4-8b04-4865-be62-4cff34f4e0c6
Retrieval Hint: "SharedWorker load error onerror bound on port MessagePort has no error event onWorkerError"
Authored by ⚖️ Ada · @neo-opus-ada · Claude Opus 5.5 · Claude Code
Context
Found on 2026-09-23 while answering a design question on D#18730 (DC 18564414). The v13.2 crash slice needs to know when a window group's canvas SharedWorker fails to start. Today no SharedWorker can report that.
The Problem
Neo.worker.Manager#createWorker(src/worker/Manager.mjs) binds its error handler like this:(isShared ? worker.port : worker).onerror = me.onWorkerError.bind(me);For a dedicated
Workerthat is right:errorfires on the worker. For aSharedWorkerit is not. A load or parse failure fireserroron theSharedWorkerobject, whileworker.portis aMessagePort, which has noerrorevent. Assigningport.onerrorcreates a property that nothing ever calls.Measured in Chromium 152, with a SharedWorker whose script returns 404:
sharedWorker.onerrorsharedWorker.port.onerror'onerror' in MessagePort.prototypeisfalse. The control, a SharedWorker that loads, exchanged a message and fired no error.So
onWorkerErrornever runs for a SharedWorker, although its own comment (written in #18567) calls it "the ONLY signal for a SharedWorker that fails to load or parse". The binding moved onto the port in 2020 (9a720bd634, which switchedonmessageonto the port to avoid callingport.start()), andonerrormoved with it. #18566's AC-6 later rewrote the handler's body but not where it is bound.The Architectural Reality
createWorkercreates every worker: App, Data, VDom, Canvas and Task. A worker is shared whensharedWorkersEnabled && Neo.config.useSharedWorkers.onmessageonworker.portis correct and must stay: messages do arrive on the port.onWorkerErrorlogs is not part of this issue.The Fix
In
createWorker, bindonerroron the worker object in both modes:(isShared ? worker.port : worker).onmessage = me.onWorkerMessage.bind(me); worker.onerror = me.onWorkerError.bind(me);The Test
Add
test/playwright/unit/worker/ManagerWorkerError.spec.mjs.Neo.worker.Managerreadswindowwhen its module loads, so its specs run it in a child process with a stubbed DOM. Copy the harness fromtest/playwright/unit/worker/ManagerReplySettlement.spec.mjs(runReplyProbe). In the child script:globalThis.WorkerandglobalThis.SharedWorkerwith a fake class whose instances haveport = {}and a method that fires a load error the way the browser does:this.onerror?.({type: 'error'});createWorker, replaceWorkerManager.onWorkerErrorwith a function that counts its calls;WorkerManager.sharedWorkersEnabledandNeo.config.useSharedWorkersfor the arm;WorkerManager.createWorker({fileName: 'App.mjs', basePath: '/'}), fire the error, and print the count.Two arms:
1. On today'sdevit is0: this exact harness was run before filing, and it printed{"onWorkerErrorCalls":0,"boundOnWorkerObject":false,"boundOnPort":true}.1before and after the fix. It proves the harness can see a call at all.How to run it
npm install npm run bundle-browser-deps npm run test-unit -- test/playwright/unit/worker/ManagerWorkerError.spec.mjsbundle-browser-depsmatters: without it the unit tier selects zero tests instead of failing one. Do not run a barenpx playwright test, because this repository has several Playwright configurations.Acceptance Criteria
createWorkerbindsonerroron the worker object for bothSharedWorkerandWorker;onmessagestays on the port for aSharedWorker.onWorkerError's body is unchanged.Out of Scope
onWorkerErrorlogs, and at which level.worker.Base#forwardErrorToMainThread(#18566).Related
onWorkerError's body but not its binding. This is their successor; the closed ticket is not reopened.Sweeps
query_raw_memories("SharedWorker fails to load silently no error reported onWorkerError worker.port onerror Manager createWorker"), 6 results, no prior decision found.src/worker.onWorkerErrorfinds only #18566 (closed, the predecessor above).unowned-rationale: a contained first issue with a red-first test already proven, left for a contributor. If nobody has claimed it by 2026-09-26, a maintainer takes it, because D#18730's slice depends on it.
Decision Record impact: none
Origin Session ID: 3be453e4-8b04-4865-be62-4cff34f4e0c6 Retrieval Hint: "SharedWorker load error onerror bound on port MessagePort has no error event onWorkerError"
Authored by ⚖️ Ada ·
@neo-opus-ada· Claude Opus 5.5 · Claude Code