Context
A SharedWorkers client workspace (embedded-editor app, private repo) runs an app-worker unhandledrejection logger. Its field log shows unhandled rejection: undefined — a rejection carrying literally zero information. A CDP attach to the app SharedWorker (Playwright page consoles cannot see shared workers) reproduced the signature on demand and pinned the origin: Runtime.exceptionThrown, text Uncaught (in promise), reason undefined, at src/worker/Base.mjs:411.
Live latest-open sweep: checked latest 20 open issues at 2026-08-30T22:15Z; no equivalent found. Closest neighbors: #17888 AC-4 (Manager forwarded-rejection envelope — different surface, see Out of Scope), #16623 (introduced the DragZone sentinel consumption this ticket must preserve), #8896 (node-side rejection handling, unrelated realm).
The Problem
Two engine seams reject bare:
src/worker/Base.mjs:411 — promiseMessage's dead-port branch: when sendMessage resolves no live port (a window closed; SharedWorkers), it calls reject() with no reason.
src/main/addon/Stylesheet.mjs:153 — a stylesheet link error handler calls reject() with no reason, so a 404'd theme file surfaces as reasonless.
A bare rejection is undiagnosable at every consumer: uncaught, it prints undefined (the field sighting); caught, it cannot be told apart from any other bare rejection. In the field case the caller was identifiable only by CDP-attaching to the worker — the log line itself said nothing.
The constraint that shapes the fix: the dead-port undefined is currently a LOAD-BEARING sentinel. src/draggable/DragZone.mjs:445 (from #16623) discriminates reason !== undefined to settle closed-window proxy-cleanup silently — the node died with its window, cleanup is moot — while logging reasoned rejections as real delta failures. A naive reject(new Error(...)) would turn every cross-window drag teardown into console noise. Sweep receipt: DragZone:445 is the only reason/err === undefined discriminator in src/.
The Architectural Reality
promiseMessage (src/worker/Base.mjs:402) is the promise seam over sendMessage, whose JSDoc already documents the undefined-return no-port contract ("callers which require delivery must check it"). The reject happens locally in the calling worker — no serialization boundary for the reason object itself.
- When the call is a remote-method proxy,
opts carries remoteClassName + remoteMethod (src/worker/mixin/RemoteMethodAccess.mjs generateRemote) — the exact context a diagnosable message needs (the field case would have read Neo.main.addon.DockFlip.captureFirst instead of undefined).
- #17888 AC-4 (in flight, @neo-gpt) normalizes the Manager envelope for forwarded rejections, where only
err.message-shaped data may survive. The discriminator must therefore live in BOTH a structured field (code) and the message string, so a forwarded dead-port rejection stays recognizable where only the message survives.
The Fix
src/worker/Base.mjs promiseMessage: reject with a typed error —
Object.assign(new Error('worker.Base#promiseMessage: no live port for destination "<dest>" (<action>[ <remoteClassName>.<remoteMethod>]) — a window closed?'), {code: 'NEO_DEAD_PORT'}).
src/draggable/DragZone.mjs:445: migrate the discriminator from reason !== undefined to reason?.code !== 'NEO_DEAD_PORT'; update the in-place comment to name the typed sentinel.
src/main/addon/Stylesheet.mjs:153: reject with an Error naming the failed href.
- Specs: new
test/playwright/unit/worker/Base.spec.mjs (deliberately NOT ReplyLoss.spec.mjs, which #17888 is editing) proving the dead-port rejection carries code: 'NEO_DEAD_PORT' + destination + remote context in the message; a DragZone discriminator pair proving silent-settle on NEO_DEAD_PORT and a logged failure on any other reason.
Contract Ledger
| Target Surface |
Source of Authority |
Proposed Behavior |
Fallback |
Docs |
Evidence |
promiseMessage dead-port rejection reason |
src/worker/Base.mjs |
Error with code: 'NEO_DEAD_PORT', message names destination + remote context |
n/a (no config) |
JSDoc at the seam + DragZone comment |
CDP receipt + Base.spec.mjs |
| DragZone silent-settle discriminator |
src/draggable/DragZone.mjs |
keys on reason?.code === 'NEO_DEAD_PORT' |
n/a |
in-place comment |
discriminator spec pair |
| Stylesheet link-error rejection |
src/main/addon/Stylesheet.mjs |
Error naming the failed href |
n/a |
JSDoc |
unit assertion |
Decision Record impact: none.
Acceptance Criteria
Out of Scope
- The Manager forwarded-rejection envelope (
src/worker/Manager.mjs) — owned by #17888 AC-4 (@neo-gpt); this ticket touches neither Manager.mjs nor ReplyLoss.spec.mjs.
- Any change to WHEN the dead-port branch fires — the port-resolution cascade is untouched; only the rejection's payload changes.
Related
#17888 (adjacent in-flight envelope work), #16623 (sentinel origin), #8149 (windowId routing migration), #17885/#17891 (the client-workspace investigation this fell out of).
Origin Session ID: b51135ac-ec45-4689-ac45-0e99fb073069
Retrieval Hint: "dead-port rejection sentinel DragZone NEO_DEAD_PORT"
Context
A SharedWorkers client workspace (embedded-editor app, private repo) runs an app-worker
unhandledrejectionlogger. Its field log showsunhandled rejection: undefined— a rejection carrying literally zero information. A CDP attach to the app SharedWorker (Playwright page consoles cannot see shared workers) reproduced the signature on demand and pinned the origin:Runtime.exceptionThrown, textUncaught (in promise), reasonundefined, atsrc/worker/Base.mjs:411.Live latest-open sweep: checked latest 20 open issues at 2026-08-30T22:15Z; no equivalent found. Closest neighbors: #17888 AC-4 (Manager forwarded-rejection envelope — different surface, see Out of Scope), #16623 (introduced the DragZone sentinel consumption this ticket must preserve), #8896 (node-side rejection handling, unrelated realm).
The Problem
Two engine seams reject bare:
src/worker/Base.mjs:411—promiseMessage's dead-port branch: whensendMessageresolves no live port (a window closed; SharedWorkers), it callsreject()with no reason.src/main/addon/Stylesheet.mjs:153— a stylesheetlinkerror handler callsreject()with no reason, so a 404'd theme file surfaces as reasonless.A bare rejection is undiagnosable at every consumer: uncaught, it prints
undefined(the field sighting); caught, it cannot be told apart from any other bare rejection. In the field case the caller was identifiable only by CDP-attaching to the worker — the log line itself said nothing.The constraint that shapes the fix: the dead-port
undefinedis currently a LOAD-BEARING sentinel.src/draggable/DragZone.mjs:445(from #16623) discriminatesreason !== undefinedto settle closed-window proxy-cleanup silently — the node died with its window, cleanup is moot — while logging reasoned rejections as real delta failures. A naivereject(new Error(...))would turn every cross-window drag teardown into console noise. Sweep receipt: DragZone:445 is the onlyreason/err === undefineddiscriminator insrc/.The Architectural Reality
promiseMessage(src/worker/Base.mjs:402) is the promise seam oversendMessage, whose JSDoc already documents the undefined-return no-port contract ("callers which require delivery must check it"). The reject happens locally in the calling worker — no serialization boundary for the reason object itself.optscarriesremoteClassName+remoteMethod(src/worker/mixin/RemoteMethodAccess.mjsgenerateRemote) — the exact context a diagnosable message needs (the field case would have readNeo.main.addon.DockFlip.captureFirstinstead ofundefined).err.message-shaped data may survive. The discriminator must therefore live in BOTH a structured field (code) and the message string, so a forwarded dead-port rejection stays recognizable where only the message survives.The Fix
src/worker/Base.mjspromiseMessage: reject with a typed error —Object.assign(new Error('worker.Base#promiseMessage: no live port for destination "<dest>" (<action>[ <remoteClassName>.<remoteMethod>]) — a window closed?'), {code: 'NEO_DEAD_PORT'}).src/draggable/DragZone.mjs:445: migrate the discriminator fromreason !== undefinedtoreason?.code !== 'NEO_DEAD_PORT'; update the in-place comment to name the typed sentinel.src/main/addon/Stylesheet.mjs:153: reject with an Error naming the failed href.test/playwright/unit/worker/Base.spec.mjs(deliberately NOTReplyLoss.spec.mjs, which #17888 is editing) proving the dead-port rejection carriescode: 'NEO_DEAD_PORT'+ destination + remote context in the message; a DragZone discriminator pair proving silent-settle onNEO_DEAD_PORTand a logged failure on any other reason.Contract Ledger
promiseMessagedead-port rejection reasonsrc/worker/Base.mjsErrorwithcode: 'NEO_DEAD_PORT', message names destination + remote contextBase.spec.mjssrc/draggable/DragZone.mjsreason?.code === 'NEO_DEAD_PORT'src/main/addon/Stylesheet.mjsErrornaming the failed hrefDecision Record impact: none.
Acceptance Criteria
promiseMessage's dead-port branch rejects with anErrorwhosecode === 'NEO_DEAD_PORT'and whose message names the destination and, for remote-method calls,remoteClassName.remoteMethod.DragZone#destroyDragProxysettles silently exactly forNEO_DEAD_PORTreasons and logs every other rejection — a spec pair proves both arms.Errornaming the failed href.reject()calls remain insrc/.Out of Scope
src/worker/Manager.mjs) — owned by #17888 AC-4 (@neo-gpt); this ticket touches neitherManager.mjsnorReplyLoss.spec.mjs.Related
#17888 (adjacent in-flight envelope work), #16623 (sentinel origin), #8149 (windowId routing migration), #17885/#17891 (the client-workspace investigation this fell out of).
Origin Session ID: b51135ac-ec45-4689-ac45-0e99fb073069 Retrieval Hint: "dead-port rejection sentinel DragZone NEO_DEAD_PORT"