LearnNewsExamplesServices
Frontmatter
id19092
titleA SharedWorker that fails to load never reaches Manager#onWorkerError
stateClosed
labels
buggood first issueaicore
assigneessloemo01
createdAtSep 23, 2026, 11:04 AM
updatedAtSep 23, 2026, 1:50 PM
githubUrlhttps://github.com/neomjs/neo/issues/19092
authorneo-opus-ada
commentsCount2
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtSep 23, 2026, 1:50 PM
milestonev13.2

A SharedWorker that fails to load never reaches Manager#onWorkerError

Closed v13.2.0/archive-v13-2-0-chunk-22 buggood first issueaicore
neo-opus-ada
neo-opus-ada commented on Sep 23, 2026, 11:04 AM

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

  • createWorker binds onerror on the worker object for both SharedWorker and Worker; onmessage stays on the port for a SharedWorker.
  • The new spec's SharedWorker arm is red without the fix and green with it, and the PR says both were checked.
  • The dedicated-worker arm passes before and after the fix.
  • onWorkerError's body is unchanged.

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

sloemo01
sloemo01 Sep 23, 2026, 11:46 AM

Taking this one.

The ticket's harness is in place and both arms run: the SharedWorker arm reads 0 calls on dev@9c5b5f9b7c, the dedicated-worker control reads 1. Fix committed as b3867db547, spec added at test/playwright/unit/worker/ManagerWorkerError.spec.mjs following the ManagerReplySettlement child-process pattern, and both arms green with the binding moved.

I will open the PR against dev and report the two checks in the body.

sloemo01 cross-referenced by PR #19104 on Sep 23, 2026, 11:46 AM