Skip to main content
Technology & EngineeringNodejs Patterns166 lines

Event Emitter

EventEmitter patterns for building decoupled, event-driven architectures in Node.js

Quick Summary16 lines
You are an expert in Node.js EventEmitter patterns for building decoupled, event-driven application architectures.

## Key Points

- Always attach an `'error'` listener on any EventEmitter that may emit errors, or enable `captureRejections` for async listeners.
- Use `events.once()` (promise-based) instead of manually wrapping `emitter.once()` in a Promise.
- Use `events.on()` for async iteration when processing an indefinite sequence of events.
- Call `emitter.removeAllListeners(event)` or `emitter.off()` during cleanup to prevent leaks — especially in tests and hot-reloading scenarios.
- Prefer composition (holding an EventEmitter as a property) over inheritance when your class is not fundamentally an event source.
- **Missing `'error'` handler** — an unhandled `'error'` event crashes the process. This is the single most common EventEmitter bug.
- **Listener leaks** — adding listeners in a loop or on each request without removing them triggers the MaxListenersExceededWarning and eventually exhausts memory.
- **Assuming asynchronous emission** — `emit()` calls listeners synchronously. Long-running listener code blocks the event loop.
- **Relying on listener order for logic** — while listeners fire in registration order, coupling behavior to ordering is fragile and hard to debug.
- **Using `removeListener` with anonymous functions** — you cannot remove a listener unless you hold a reference to the original function.
skilldb get nodejs-patterns-skills/Event EmitterFull skill: 166 lines
Paste into your CLAUDE.md or agent config

EventEmitter — Node.js Patterns

You are an expert in Node.js EventEmitter patterns for building decoupled, event-driven application architectures.

Core Philosophy

Overview

EventEmitter is the backbone of Node.js's event-driven architecture. Streams, HTTP servers, and most core modules extend it. Understanding EventEmitter deeply — including memory management, error conventions, and composition patterns — is essential for building robust Node.js applications and libraries.

Core Concepts

The Observer Pattern

EventEmitter implements the observer pattern: objects register listener functions for named events, and emitters invoke those listeners synchronously when the event fires. This decouples the producer of events from consumers.

Error Event Convention

The 'error' event has special behavior: if emitted and no listener is registered, Node.js throws the error and crashes the process. Every EventEmitter that can fail must either have an 'error' listener or use captureRejections.

Listener Lifecycle

Listeners are called in registration order. They can be one-shot (once) or persistent (on). The default maximum listener count is 10, which serves as a leak-detection warning, not a hard limit.

Implementation Patterns

Typed EventEmitter with composition

const { EventEmitter } = require('node:events');

class JobQueue extends EventEmitter {
  #jobs = [];

  add(job) {
    this.#jobs.push(job);
    this.emit('added', job);
  }

  async processNext() {
    const job = this.#jobs.shift();
    if (!job) return;

    this.emit('processing', job);
    try {
      const result = await job.execute();
      this.emit('completed', job, result);
    } catch (err) {
      this.emit('failed', job, err);
    }
  }
}

const queue = new JobQueue();
queue.on('added', (job) => console.log(`Queued: ${job.name}`));
queue.on('failed', (job, err) => console.error(`${job.name} failed:`, err));

AbortSignal integration

const { EventEmitter } = require('node:events');

async function waitForEvent(emitter, event, { signal } = {}) {
  return new Promise((resolve, reject) => {
    if (signal?.aborted) return reject(signal.reason);

    const onEvent = (...args) => {
      signal?.removeEventListener('abort', onAbort);
      resolve(args);
    };

    const onAbort = () => {
      emitter.removeListener(event, onEvent);
      reject(signal.reason);
    };

    emitter.once(event, onEvent);
    signal?.addEventListener('abort', onAbort, { once: true });
  });
}

// Usage
const ac = new AbortController();
setTimeout(() => ac.abort(), 5000);
const [data] = await waitForEvent(server, 'connection', { signal: ac.signal });

Async iteration over events (events.on)

const { on } = require('node:events');

async function handleRequests(server) {
  for await (const [req, res] of on(server, 'request')) {
    res.writeHead(200);
    res.end('OK');
  }
}

EventTarget compatibility

const { EventEmitter } = require('node:events');

// Node.js EventEmitter can listen to EventTarget-style events
const ac = new AbortController();

EventEmitter.once(ac.signal, 'abort').then(() => {
  console.log('Abort signal received');
});

ac.abort();

Capture rejections for async listeners

const { EventEmitter } = require('node:events');

const emitter = new EventEmitter({ captureRejections: true });

emitter.on('data', async (chunk) => {
  const result = await processAsync(chunk);
  if (!result) throw new Error('Processing failed');
});

// Rejected promises in async listeners are routed here
emitter.on('error', (err) => {
  console.error('Async listener error:', err.message);
});

Best Practices

  • Always attach an 'error' listener on any EventEmitter that may emit errors, or enable captureRejections for async listeners.
  • Use events.once() (promise-based) instead of manually wrapping emitter.once() in a Promise.
  • Use events.on() for async iteration when processing an indefinite sequence of events.
  • Call emitter.removeAllListeners(event) or emitter.off() during cleanup to prevent leaks — especially in tests and hot-reloading scenarios.
  • Prefer composition (holding an EventEmitter as a property) over inheritance when your class is not fundamentally an event source.

Common Pitfalls

  • Missing 'error' handler — an unhandled 'error' event crashes the process. This is the single most common EventEmitter bug.
  • Listener leaks — adding listeners in a loop or on each request without removing them triggers the MaxListenersExceededWarning and eventually exhausts memory.
  • Assuming asynchronous emissionemit() calls listeners synchronously. Long-running listener code blocks the event loop.
  • Relying on listener order for logic — while listeners fire in registration order, coupling behavior to ordering is fragile and hard to debug.
  • Using removeListener with anonymous functions — you cannot remove a listener unless you hold a reference to the original function.

Anti-Patterns

Over-engineering for hypothetical scale. Building for millions of users when you have hundreds adds complexity without value. Solve today's problems first.

Ignoring the existing ecosystem. Reinventing functionality that mature libraries already provide well wastes time and introduces unnecessary risk.

Premature abstraction. Creating elaborate frameworks and utilities before you have enough concrete cases to know what the abstraction should look like produces the wrong abstraction.

Neglecting error handling at boundaries. Internal code can trust its inputs, but system boundaries (user input, APIs, file I/O) require defensive validation.

Skipping documentation for obvious code. What is obvious to you today will not be obvious to your colleague next month or to you next year.

Install this skill directly: skilldb add nodejs-patterns-skills

Get CLI access →