Node.js EventEmitter
Use Node.js EventEmitter for typed pub-sub communication with memory leak prevention
When to Use
- Implementing decoupled communication between modules
- Building plugin systems or extensible architectures
- Replacing callback chains with event-driven patterns
- Creating typed event systems in TypeScript applications
Instructions
- Basic EventEmitter usage:
import { EventEmitter } from 'node:events';
const emitter = new EventEmitter();
emitter.on('user:created', (user: { id: string; name: string }) => {
console.log(`User created: ${user.name}`);
});
emitter.emit('user:created', { id: '1', name: 'Alice' });
- Typed EventEmitter in TypeScript:
import { EventEmitter } from 'node:events';
interface AppEvents {
'user:created': [user: { id: string; name: string }];
'user:deleted': [userId: string];
error: [error: Error];
}
class AppEmitter extends EventEmitter<AppEvents> {}
const emitter = new AppEmitter();
emitter.on('user:created', (user) => {
// user is typed as { id: string; name: string }
console.log(user.name);
});
- One-time listeners with
once:
emitter.once('ready', () => {
console.log('System initialized');
});
- Remove listeners to prevent memory leaks:
const handler = (data: string) => console.log(data);
emitter.on('data', handler);
// Later: remove the specific listener
emitter.off('data', handler);
// Or remove all listeners for an event
emitter.removeAllListeners('data');
- Set max listeners to catch leaks early:
emitter.setMaxListeners(20); // Default is 10
// Or globally
EventEmitter.defaultMaxListeners = 20;
- Async event handling with
onceas a Promise:
import { once } from 'node:events';
const [data] = await once(emitter, 'data');
console.log(data);
- Error handling — always listen for
'error'events:
emitter.on('error', (err) => {
console.error('Emitter error:', err);
});
// Without an error listener, emitting 'error' throws and crashes the process
- AbortController for cleanup:
const ac = new AbortController();
emitter.on('data', handler, { signal: ac.signal });
// Automatically removes the listener
ac.abort();
Details
EventEmitter is Node.js's built-in pub-sub mechanism. It is synchronous by default — emit() calls listeners in registration order and blocks until all complete.
Synchronous emission: emitter.emit('event', data) runs all listeners synchronously in the current tick. Long-running listeners block the event loop. Use setImmediate or queueMicrotask inside listeners for async work.
Memory leak warning: If more than maxListeners are registered for a single event, Node.js prints a warning. This usually indicates listeners being added in a loop without removal.
Event ordering: Listeners fire in registration order. prependListener adds to the front of the queue.
Trade-offs:
- EventEmitter is built-in and zero-dependency — but is synchronous by default
- Typed events in TypeScript catch event name typos — but require maintaining a type map
- Decoupled communication is flexible — but makes control flow harder to trace
onceas Promise is convenient — but only captures the first emission
Source
https://nodejs.org/api/events.html
Process
- Read the instructions and examples in this document.
- Apply the patterns to your implementation, adapting to your specific context.
- Verify your implementation against the details and edge cases listed above.
Harness Integration
- Type: knowledge — this skill is a reference document, not a procedural workflow.
- No tools or state — consumed as context by other skills and agents.
Success Criteria
- The patterns described in this document are applied correctly in the implementation.
- Edge cases and anti-patterns listed in this document are avoided.