Filed by GitHub Copilot on behalf of Joaquín Ruales (@jruales). Root cause verified in source; fix validated by a controlled A/B in a real VS Code window.
Summary
A single listener exception permanently wedges EventEmitter, which silently kills the CDP message pump for that connection. When it happens during target attach, js-debug never sends Runtime.runIfWaitingForDebugger, so the debuggee stays paused at startup forever.
Symptom when debugging an Electron app (the microsoft/vscode workbench): the window renders but accepts no input — nothing clickable, keyboard dead, command palette opens but neither responds nor dismisses.
Root cause
1. EventEmitter.fire doesn't restore _deliveryQueue when a listener throws — src/common/events.ts#L55-L65
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 here
}
this._deliveryQueue = undefined; // never reached
_deliveryQueue stays truthy, so every later fire() computes dispatch === false, enqueues, and returns without dispatching. The emitter is permanently deaf. Since Connection consumes its transport via this._transport.onMessage(...) (connection.ts#L59), one bad message permanently stops all CDP traffic on that connection.
2. Connection._onMessage throws on an unknown session id — src/cdp/connection.ts#L104-L110
Messages for disposed sessions are ignored, but messages for unknown sessions throw — which is what enters the wedge above.
Why an unknown session id occurs
Inspector.workerScriptLoaded is a Blink event fired from WorkerGlobalScope::RunWorkerScript() when a web worker finishes evaluating its script. It is never handled by js-debug at runtime (it appears only in the generated src/cdp/api.d.ts), so it arrives unsolicited.
Session registration appears to be deferred: Target.setAutoAttach({ flatten: true }) delivers worker targets, but the targetCreated handler is wrapped in enqueueLifecycleFn (browserTargetManager.ts#L171-L174), so createSession(sessionId) runs on a later microtask. A workerScriptLoaded landing in that window finds no registered session.
This matches the observed behaviour: it is intermittent, and it reproduces with workbench web workers such as editorWorkerService.
Evidence
Error at the exact moment of the freeze, with EventEmitter.fire directly beneath the throw:
[error] 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)
The debuggee was confirmed waiting for the debugger, not paused at a breakpoint:
Runtime.evaluate of synchronous code succeeded
setTimeout / requestAnimationFrame never fired; event handlers never ran
Debugger.resume returned Can only perform operation while paused
Runtime.runIfWaitingForDebugger immediately restored the window
Minimal repro against the unmodified EventEmitter:
const emitter = new EventEmitter<string>();
const delivered: string[] = [];
emitter.event(v => { if (v === 'poison') throw new Error('boom'); delivered.push(v); });
emitter.fire('before');
try { emitter.fire('poison'); } catch {}
emitter.fire('after-1');
console.log(delivered); // ["before"] <-- 'after-1' is never delivered
Fix
#2402 restores _deliveryQueue in a finally block, and treats unknown sessions the way disposed ones are already treated (warn and ignore).
Validated by a controlled A/B: the extension built both ways, loaded into a real VS Code Insiders window via --extensionDevelopmentPath, same machine/repo/profile/launch config, only the diff varying.
| Build |
Outcome |
| Unpatched |
Workbench freezes. Reproduced. |
| Patched |
Works every time, across repeated runs. |
Environment
|
|
| js-debug |
1.117.0 (bundled with VS Code Insiders) |
| VS Code |
1.136.0-insider |
| Electron (debuggee) |
42.8.1 |
| OS |
macOS (arm64) |
| Scenario |
Debugging microsoft/vscode via its Renderer and Main processes compound |
Summary
A single listener exception permanently wedges
EventEmitter, which silently kills the CDP message pump for that connection. When it happens during target attach, js-debug never sendsRuntime.runIfWaitingForDebugger, so the debuggee stays paused at startup forever.Symptom when debugging an Electron app (the
microsoft/vscodeworkbench): the window renders but accepts no input — nothing clickable, keyboard dead, command palette opens but neither responds nor dismisses.Root cause
1.
EventEmitter.firedoesn't restore_deliveryQueuewhen a listener throws —src/common/events.ts#L55-L65_deliveryQueuestays truthy, so every laterfire()computesdispatch === false, enqueues, and returns without dispatching. The emitter is permanently deaf. SinceConnectionconsumes its transport viathis._transport.onMessage(...)(connection.ts#L59), one bad message permanently stops all CDP traffic on that connection.2.
Connection._onMessagethrows on an unknown session id —src/cdp/connection.ts#L104-L110Messages for disposed sessions are ignored, but messages for unknown sessions throw — which is what enters the wedge above.
Why an unknown session id occurs
Inspector.workerScriptLoadedis a Blink event fired fromWorkerGlobalScope::RunWorkerScript()when a web worker finishes evaluating its script. It is never handled by js-debug at runtime (it appears only in the generatedsrc/cdp/api.d.ts), so it arrives unsolicited.Session registration appears to be deferred:
Target.setAutoAttach({ flatten: true })delivers worker targets, but thetargetCreatedhandler is wrapped inenqueueLifecycleFn(browserTargetManager.ts#L171-L174), socreateSession(sessionId)runs on a later microtask. AworkerScriptLoadedlanding in that window finds no registered session.This matches the observed behaviour: it is intermittent, and it reproduces with workbench web workers such as
editorWorkerService.Evidence
Error at the exact moment of the freeze, with
EventEmitter.firedirectly beneath the throw:The debuggee was confirmed waiting for the debugger, not paused at a breakpoint:
Runtime.evaluateof synchronous code succeededsetTimeout/requestAnimationFramenever fired; event handlers never ranDebugger.resumereturnedCan only perform operation while pausedRuntime.runIfWaitingForDebuggerimmediately restored the windowMinimal repro against the unmodified
EventEmitter:Fix
#2402 restores
_deliveryQueuein afinallyblock, and treats unknown sessions the way disposed ones are already treated (warn and ignore).Validated by a controlled A/B: the extension built both ways, loaded into a real VS Code Insiders window via
--extensionDevelopmentPath, same machine/repo/profile/launch config, only the diff varying.Environment
microsoft/vscodevia itsRenderer and Main processescompound