Skip to content
Node.js nd events 4 min read

Handling the 'error' Event

Among all the events an EventEmitter can carry, one name is special: error. Node.js treats 'error' as a last-resort signal of failure, and it enforces a hard rule — if an emitter fires error and no listener is attached, the error is thrown rather than swallowed, typically taking the whole process down. This behavior is deliberate: a silently dropped error is far more dangerous than a loud crash. Understanding the rule, and the handful of APIs around it, is essential for writing emitters and streams that fail safely.

Why an unhandled error crashes the process

When you call emit('error', err) on an EventEmitter that has no error listener, the emitter throws the error value. Because emit runs synchronously and there is usually no surrounding try/catch at the point a stream or socket emits, the throw propagates to the top of the stack and becomes an uncaught exception — which by default terminates the Node process.

import { EventEmitter } from 'node:events';

const emitter = new EventEmitter();

// No 'error' listener is registered.
emitter.emit('error', new Error('something went wrong'));

console.log('This line never runs');

Output:

node:events:496
      throw er; // Unhandled 'error' event
      ^

Error: something went wrong
    at file:///app/index.js:6:18
    ...
Emitted 'error' event on EventEmitter instance at:
    ...

The process exits with a non-zero code and 'This line never runs' is never printed. This is not a bug — it is the documented contract. Any emitter that can fail (HTTP servers, streams, child processes, sockets) can emit error, so each one needs a handler.

Catching the throw with a surrounding try/catch only works when emit('error') is called synchronously in your own code. For built-in emitters that emit asynchronously — a stream failing mid-read, a socket timing out — there is no synchronous call to wrap. You must attach an error listener instead.

Handling errors correctly

The fix is simple: register a listener for the 'error' event. Once at least one listener exists, the emitter no longer throws — it just invokes your handler with the error value, exactly like any other event.

import { EventEmitter } from 'node:events';

const emitter = new EventEmitter();

emitter.on('error', (err) => {
  console.error('Handled error:', err.message);
});

emitter.emit('error', new Error('connection refused'));

console.log('Execution continues normally');

Output:

Handled error: connection refused
Execution continues normally

For long-lived emitters such as servers and streams, attach the error listener before any operation that could fail, so a failure during setup is never missed.

import { createReadStream } from 'node:fs';

const stream = createReadStream('/path/that/does/not/exist.txt');

stream.on('error', (err) => {
  console.error(`Stream failed: ${err.code}`);
});

stream.on('data', (chunk) => console.log(`Read ${chunk.length} bytes`));

Output:

Stream failed: ENOENT

Without that error listener, the missing file would emit error, find no handler, and crash the program.

Monitoring errors with errorMonitor

Sometimes you want to observe errors — for logging, metrics, or tracing — without changing the special throw-on-unhandled behavior. A normal .on('error', fn) listener disables the crash, which can mask configuration mistakes elsewhere. The events.errorMonitor symbol solves this: a listener installed under this symbol is called whenever an error is emitted, but it does not count as a regular handler. If no real error listener exists, the error still throws after your monitor runs.

import { EventEmitter, errorMonitor } from 'node:events';

const emitter = new EventEmitter();

emitter.on(errorMonitor, (err) => {
  console.log('Monitor saw:', err.message); // observe, do not handle
});

emitter.on('error', (err) => {
  console.log('Real handler:', err.message); // this one prevents the crash
});

emitter.emit('error', new Error('disk full'));

Output:

Monitor saw: disk full
Real handler: disk full

If you remove the real 'error' handler above, the monitor still logs disk full, and then the process crashes — which is exactly the point. The monitor lets observability code see every error without accidentally suppressing the safety net.

ApproachReceives errorPrevents crashUse for
.on('error', fn)YesYesActual error handling and recovery
.on(errorMonitor, fn)YesNoLogging, metrics, tracing
No listenerNo (throws)Never acceptable on failable emitters

The process-level safety net

For truly unexpected cases, process.on('uncaughtException', ...) catches errors that escaped every emitter. Treat this as a place to log and shut down gracefully, not as routine error handling — the process is in an undefined state afterward and should exit.

process.on('uncaughtException', (err) => {
  console.error('Fatal, shutting down:', err.message);
  process.exit(1);
});

Best Practices

  • Attach an error listener to every emitter that can fail — servers, streams, sockets, and child processes — before you start using it.
  • Register the listener before triggering the operation, so failures during setup are not missed.
  • Never leave a failable emitter without an error handler; an unhandled error event throws and crashes the process.
  • Use events.errorMonitor for logging and metrics so observability code never accidentally suppresses the crash-on-unhandled safety behavior.
  • Keep a single real error handler responsible for recovery; use the monitor symbol for everything that only needs to observe.
  • Reserve process.on('uncaughtException') as a last-resort logger that exits the process, not as a substitute for per-emitter handlers.
Last updated June 14, 2026
Was this helpful?