NodeJSHandling error Events

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

JS
import { EventEmitter } from 'node:events'
const e = new EventEmitter()

e.emit('error', new Error('boom'))   // no 'error' listener attached
node:events:498
      throw er; // Unhandled 'error' event
      ^
Error: boom
    at Object.<anonymous> ...
[process exits with code 1]
Unhandled `error` = uncaught exception = process death
When `emit('error', err)` finds no listener, `EventEmitter` *throws* the error. With nothing to catch it, that becomes an uncaught exception and Node terminates with exit code 1. This is **intentional** — a silently-swallowed error is worse than a crash. The fix is never to suppress it, but to always attach a handler.
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.

The contract
Any emitter that can fail is expected to fire `'error'`, and any code that owns such an emitter is expected to listen. Node's built-ins — `http.Server`, sockets, streams, `child_process` — all follow this. So must yours: emit `'error'` on failure rather than throwing inside an async callback where no one can catch it.
Always attach a handler

JS
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:

JS
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))
Each stream in a pipeline needs error handling
Chaining `a.pipe(b).pipe(c)` does **not** forward errors — if `a` errors, `b` and `c` aren't told, and `a`'s unhandled `'error'` crashes you. Either attach `'error'` to every stream, or use `stream.pipeline()` (or `pipeline` from `node:stream/promises`), which propagates errors and cleans up the whole chain for you.
error events vs thrown errors vs rejections

Failure surfaces as

Caught by

If unhandled

Synchronous throw

try/catch

uncaughtException → crash

Emitter 'error' event

.on('error', …)

Thrown → crash

Rejected promise

.catch / try around await

unhandledRejection → crash (modern Node)

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:

JS
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)
})
`uncaughtException` is not a `try/catch` substitute
After an uncaught exception the process is in an *undefined* state — half-finished operations, leaked resources, corrupt invariants. The official guidance is to log, clean up, and **restart** (let a process manager like systemd/PM2/Kubernetes bring you back fresh). Resuming normal operation from this handler is unsafe. Handle errors at their source; use these only as a crash-logging net. See [Graceful Shutdown](/nodejs/process-events).
The rules
  • Every emitter that can fail gets an error listener — 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.on nets are for logging + restart, never for resuming as if nothing happened.

Section complete
That wraps up Events & EventEmitter. Next, the engine underneath it all: [Asynchronous Programming in Node](/nodejs/async-intro).