Summary
On Windows, the conin (write side) socket is constructed without an 'error' listener, so any write failure on it becomes an uncaught exception and takes down the host process. The conout (read side) socket does get one.
Observed as Error: write EAGAIN (errno: -4088) escaping as an uncaught exception, with no 'error' event available on anything the embedder owns.
Version: 1.2.0-beta.14 (line references below are from the published lib/, and match the compiled output 1:1).
The asymmetry
conout gets a listener — windowsTerminal.js:54 assigns it and :90 attaches the handler:
this._socket = this._agent.outSocket;
// ...
this._socket.on('error', (err) => { /* ... */ });
conin does not. windowsPtyAgent.js:79:
const inSocketFD = fs.openSync(term.conin, 'w');
this._inSocket = new net.Socket({ fd: inSocketFD, readable: false, writable: true });
this._inSocket.setEncoding('utf8');
Grepping the package, _inSocket never receives an 'error' handler anywhere.
Why that is load-bearing
_inSocket is the write path — windowsTerminal.js:123:
this._agent.inSocket.write(data);
and kill() destroys it — windowsPtyAgent.js:176:
this._inSocket.destroy();
A net.Socket with no 'error' listener re-throws on error, so both of these can crash the process:
- backpressure — EAGAIN when conin's buffer fills under sustained input
- a write racing
kill() — the socket is destroyed while a write is in flight
Because the throw originates inside node-pty's own plumbing, an embedder cannot prevent it. Terminal.on() forwards to the conout socket, so there is no public route to conin; the only workaround is reaching through the private _agent to inSocket and attaching the listener from outside, which is what we currently do.
Suggested fix
Attach a handler where the socket is created, mirroring the conout side — even a no-op prevents the process-level throw and lets the existing exit/close paths drive the lifecycle:
this._inSocket.on('error', () => { /* surfaced via onExit / the existing error path */ });
Routing it through _onError for parity with conout would be better still, if that suits the intended semantics.
Impact
For anything long-lived that writes to a pty and kills ptys on demand — terminal multiplexers, session managers, agent supervisors — this is a hard crash of the host process on Windows rather than a recoverable error. It presents as an intermittent uncaught exception attributed to whatever code happened to be running, which makes it easy to misfile as unrelated flakiness.
Happy to open a PR if the one-line form above is acceptable.
Summary
On Windows, the conin (write side) socket is constructed without an
'error'listener, so any write failure on it becomes an uncaught exception and takes down the host process. The conout (read side) socket does get one.Observed as
Error: write EAGAIN(errno: -4088) escaping as an uncaught exception, with no'error'event available on anything the embedder owns.Version:
1.2.0-beta.14(line references below are from the publishedlib/, and match the compiled output 1:1).The asymmetry
conout gets a listener —
windowsTerminal.js:54assigns it and:90attaches the handler:conin does not.
windowsPtyAgent.js:79:Grepping the package,
_inSocketnever receives an'error'handler anywhere.Why that is load-bearing
_inSocketis the write path —windowsTerminal.js:123:and
kill()destroys it —windowsPtyAgent.js:176:A
net.Socketwith no'error'listener re-throws on error, so both of these can crash the process:kill()— the socket is destroyed while a write is in flightBecause the throw originates inside node-pty's own plumbing, an embedder cannot prevent it.
Terminal.on()forwards to the conout socket, so there is no public route to conin; the only workaround is reaching through the private_agenttoinSocketand attaching the listener from outside, which is what we currently do.Suggested fix
Attach a handler where the socket is created, mirroring the conout side — even a no-op prevents the process-level throw and lets the existing exit/close paths drive the lifecycle:
Routing it through
_onErrorfor parity with conout would be better still, if that suits the intended semantics.Impact
For anything long-lived that writes to a pty and kills ptys on demand — terminal multiplexers, session managers, agent supervisors — this is a hard crash of the host process on Windows rather than a recoverable error. It presents as an intermittent uncaught exception attributed to whatever code happened to be running, which makes it easy to misfile as unrelated flakiness.
Happy to open a PR if the one-line form above is acceptable.