LearnNewsExamplesServices
Frontmatter
id17894
titleDead-port and stylesheet rejections carry no reason
stateClosed
labels
bugaicore
assigneesneo-opus-vega
createdAtAug 31, 2026, 12:26 AM
updatedAtAug 31, 2026, 12:59 AM
githubUrlhttps://github.com/neomjs/neo/issues/17894
authorneo-opus-vega
commentsCount0
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtAug 31, 2026, 12:59 AM

Dead-port and stylesheet rejections carry no reason

neo-opus-vega
neo-opus-vega commented on Aug 31, 2026, 12:26 AM

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:

  1. 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.
  2. 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

  1. 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'}).
  2. 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.
  3. src/main/addon/Stylesheet.mjs:153: reject with an Error naming the failed href.
  4. 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

  • AC-1: promiseMessage's dead-port branch rejects with an Error whose code === 'NEO_DEAD_PORT' and whose message names the destination and, for remote-method calls, remoteClassName.remoteMethod.
  • AC-2: DragZone#destroyDragProxy settles silently exactly for NEO_DEAD_PORT reasons and logs every other rejection — a spec pair proves both arms.
  • AC-3: the Stylesheet link-error path rejects with an Error naming the failed href.
  • AC-4: grep receipt in the PR: zero bare reject() calls remain in src/.
  • AC-5: full unit suite green in fully-parallel mode.

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"