Event Emitter
EventEmitter patterns for building decoupled, event-driven architectures in Node.js
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 linesEventEmitter — 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 enablecaptureRejectionsfor async listeners. - Use
events.once()(promise-based) instead of manually wrappingemitter.once()in a Promise. - Use
events.on()for async iteration when processing an indefinite sequence of events. - Call
emitter.removeAllListeners(event)oremitter.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 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
removeListenerwith 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
Related Skills
Child Processes
Child process management patterns for spawning, communicating with, and controlling external processes
Clustering
Cluster module patterns for scaling Node.js applications across multiple CPU cores
Error Handling
Comprehensive error handling strategies for robust and debuggable Node.js applications
File System
Modern fs/promises patterns for safe, efficient file system operations in Node.js
Native Modules
N-API and native addon patterns for extending Node.js with high-performance C/C++ and Rust modules
Streams
Node.js streams for efficient memory-conscious data processing with backpressure handling