Handling error Events
'error' is the most important event name in Node. Unlike every other event, it has special runtime behavior: an emitter that fires 'error' with no listener doesn't just do nothing — it crashes the entire process. Understanding why, and handling it correctly on every emitter, is the difference between a resilient server and one that dies on the first network hiccup.
The crash, demonstrated
import { EventEmitter } from 'node:events'
const e = new EventEmitter()
e.emit('error', new Error('boom')) // no 'error' listener attachednode:events:498
throw er; // Unhandled 'error' event
^
Error: boom
at Object.<anonymous> ...
[process exits with code 1]Why it's designed this way
Errors must never vanish. If 'error' were like any other event, firing it with no listener would do nothing — and a failed database connection or socket would be silently ignored, leaving your app in a broken state with no signal. Forcing a crash makes the missing handler impossible to overlook in development.
Always attach a handler
const e = new EventEmitter()
// This one line turns a fatal crash into a handled condition:
e.on('error', (err) => {
console.error('Handled:', err.message)
// log it, increment a metric, attempt recovery — but stay alive
})
e.emit('error', new Error('boom')) // → Handled: boom (process survives)On real Node objects
This isn't academic — forgetting the handler on a server or stream is a leading cause of production crashes:
import { createServer } from 'node:http'
import { createReadStream } from 'node:fs'
const server = createServer(handler)
server.on('error', (err) => {
if (err.code === 'EADDRINUSE') console.error('Port already in use')
})
// A stream that hits a missing file emits 'error' — handle it or crash:
const stream = createReadStream('./maybe-missing.txt')
stream.on('error', (err) => console.error('read failed:', err.code))
stream.on('data', (chunk) => process.stdout.write(chunk))error events vs thrown errors vs rejections
Failure surfaces as | Caught by | If unhandled |
|---|---|---|
Synchronous |
|
|
Emitter |
| Thrown → crash |
Rejected promise |
|
|
They're three doors to the same room. Promises and async/await are covered in Promises in Node.js; emitter errors are the event-world equivalent.
Last-resort safety nets
process-level handlers catch what slips through — but treat them as a place to log and exit cleanly, not to keep running as if nothing happened:
process.on('uncaughtException', (err) => {
console.error('FATAL uncaught:', err)
// flush logs, then exit — the process is now in an unknown state
process.exit(1)
})
process.on('unhandledRejection', (reason) => {
console.error('Unhandled rejection:', reason)
process.exit(1)
})The rules
Every emitter that can fail gets an
errorlistener — servers, sockets, streams, child processes, and your own.Attach it before the emitter starts work, so you never miss an early error.
Use
stream.pipeline()instead of bare.pipe()chains for automatic error propagation.process.onnets are for logging + restart, never for resuming as if nothing happened.