Connection reliability
Handle end, close, and errors
Distinguish a peer half-close, final socket closure, and an exceptional failure.
10 minute lesson
A TCP connection can finish several ways, and Node reports each with a distinct event. Handling all of them cleanly is the difference between a server that runs for months and one that slowly leaks state.
The end event means the readable side received FIN — the peer announced it will send no more data. TCP allows a half-close: the socket may still be writable, so you can send a final reply after the client finished sending. In Node that only happens if you create the server with allowHalfOpen: true; by default Node closes the writable side back automatically.
The close event means the handle is fully gone. It fires exactly once for every connection, however it ended. error carries a failure, and close follows it with hadError set to true.
Log the lifecycle:
socket.on('end', () => console.log('peer ended writes'))
socket.on('close', hadError => console.log('closed', { hadError }))
socket.on('error', error => console.error(error.code))
Watch each ending
Close a client normally — quit nc with Ctrl-C, which makes the kernel send FIN:
peer ended writes
closed { hadError: false }
Now force an abrupt reset. Have a test client call socket.resetAndDestroy(), which sends RST instead of FIN:
ECONNRESET
closed { hadError: true }
ECONNRESET also shows up in the wild when a peer crashes with data in flight or a middlebox kills the connection. Finally, stop the server while a client is connected and watch the client side see end — closure looks graceful from the other end too.
Compare event order across runs: error comes first when present, end appears only on graceful closes, and close is always last.
Clean up on close, not on end
Per-connection resources — timers, entries in a clients map, pending work — belong in the close handler:
socket.on('close', () => {
clearTimeout(deadline)
clients.delete(socket)
})
Cleanup timers and per-client state on close. Do not assume every connection reaches the happy-path end event: resets, crashes, and your own socket.destroy() calls all skip it. State you only release on end leaks until the process restarts, and close is the one event guaranteed to fire.
Lesson completed