Skip to content

fix: don't let a listener exception wedge EventEmitter - #2402

Closed
Joaquín Ruales (jruales) wants to merge 1 commit into
microsoft:mainfrom
jruales:fix/emitter-wedge-on-listener-throw
Closed

fix: don't let a listener exception wedge EventEmitter#2402
Joaquín Ruales (jruales) wants to merge 1 commit into
microsoft:mainfrom
jruales:fix/emitter-wedge-on-listener-throw

Conversation

@jruales

@jruales Joaquín Ruales (jruales) commented Aug 28, 2026

Copy link
Copy Markdown

Authored by GitHub Copilot on behalf of Joaquín Ruales (@jruales).

Fixes #2401

Problem

EventEmitter.fire leaves _deliveryQueue populated when a listener throws:

if (!dispatch) return;
for (let index = 0; index < this._deliveryQueue.length; index++) {
  const { data, event } = this._deliveryQueue[index];
  data.listener.call(data.thisArg, event);   // throws
}
this._deliveryQueue = undefined;             // never reached

Because the queue is never cleared, every later fire computes dispatch === false, enqueues, and returns without dispatching. One listener exception permanently stops all delivery on that emitter.

Connection reads its transport through this._transport.onMessage(...), so this silently kills the CDP message pump for the whole connection. The debuggee is then never sent Runtime.runIfWaitingForDebugger and stays paused at startup — the window paints but accepts no input.

The exception comes from Connection._onMessage, which ignores messages for disposed sessions but throws for unknown ones. Inspector.workerScriptLoaded (a Blink event fired when a web worker finishes evaluating) is never handled by js-debug and can arrive before the worker's session is registered, since targetCreated is queued through enqueueLifecycleFn:

Error: Unknown session id: B9878282765DBAAB165E4A5851627507 while processing: Inspector.workerScriptLoaded
    at ut._onMessage (.../src/extension.js:58:8166)
    at U.fire        (.../src/extension.js:39:11654)

Changes

  • src/common/events.ts — restore _deliveryQueue in a finally block so one bad listener can't wedge the emitter. This is the load-bearing fix and is independent of the CDP specifics.
  • src/cdp/connection.ts — treat unknown sessions the way disposed sessions are already treated (warn and ignore) instead of throwing.
  • src/common/events.test.ts — regression test.

Validation

  • New test fails without the events.ts change (expected [ 'before' ] to deeply equal [ 'before', 'after' ]) and passes with it.
  • npm run test:unit268 passing. tsc --noEmit and gulp lint clean.
  • End-to-end A/B: this extension built both ways, loaded into a real VS Code Insiders window via --extensionDevelopmentPath; same machine, repository (microsoft/vscode, Electron 42.8.1), isolated profile and launch compound, only this diff varying.
Build Outcome
Unpatched Workbench freezes. Reproduced.
Patched Works every time, across repeated runs.

Happy to split the connection.ts change into a separate PR if you'd prefer to evaluate the two independently.

`EventEmitter.fire` left `_deliveryQueue` populated when a listener threw,
so every later `fire` took the re-entrancy path and returned without
dispatching. One exception permanently stopped all delivery on that
emitter, which silently killed the CDP message pump for the connection
and left the debuggee waiting for `Runtime.runIfWaitingForDebugger`.

Restore the queue in a `finally` block, and treat messages for unknown
sessions the same way messages for disposed sessions are already treated
(warn and ignore) rather than throwing. Electron emits
`Inspector.workerScriptLoaded` for sessions we don't track, which made
this reachable in normal operation.

Fixes microsoft#2401

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
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.

Listener exception permanently wedges EventEmitter, deadlocking the debuggee at startup

1 participant