Event Emitters & Event-Driven Architecture
Decouple components with EventEmitter, build domain events, and avoid listener leaks in long-running servers.
EventEmitter Basics
Node EventEmitter implements the observer pattern. Objects emit named events; listeners register with on or once. Many core APIs inherit from EventEmitter: streams, servers, and process.
Extend EventEmitter in domain services to broadcast orderPlaced or userRegistered events without tight coupling between modules.
Keep event names consistent constants to avoid typos and aid discoverability across teams.
- Use once for listeners that should fire a single time
- Prefer typed event maps in TypeScript for autocomplete
- Document event payloads alongside API schemas
import { EventEmitter } from 'node:events';
class OrderBus extends EventEmitter {}
const bus = new OrderBus();
bus.on('order:created', order => console.log(order.id));
bus.emit('order:created', { id: '42' });Listener Management
Each on registration adds a listener. Duplicate registrations mean duplicate handlers unless you remove them with off or removeListener.
Set setMaxListeners when many components legitimately subscribe, but investigate leaks if warnings appear frequently.
In tests, remove listeners in afterEach to prevent cross-test pollution.
- Avoid anonymous functions if you need to remove specific listeners
- Use AbortSignal with events when integrating async cancellation
- Monitor listener counts in production diagnostics endpoints
emitter.on('data', handler);
emitter.off('data', handler);
console.log(emitter.listenerCount('data'));Error Events
EventEmitter treats error events specially. Unhandled error events throw and can crash the process. Always attach error listeners on emitters you do not control.
For domain buses, wrap listener execution in try/catch and emit error secondary events or log to centralized monitoring.
Distinguish operational errors (expected validation failures) from programmer errors (thrown exceptions in listeners).
- Register process.on("uncaughtException") only with careful shutdown logic
- Use domain-style boundaries or message queues for heavy async handlers
- Never swallow errors silently in event callbacks
Architecture Patterns
Event-driven architecture scales organizationally: producers emit facts; consumers react asynchronously. Pair in-process EventEmitter with message brokers like Redis or RabbitMQ for multi-service systems.
Avoid turning EventEmitter into a hidden global god object. Scope buses per bounded context and inject them where needed.
Combine events with idempotent handlers so retries do not double-charge customers or send duplicate emails.
- Name events in past tense: order.created, payment.captured
- Persist critical events to an outbox table for reliability
- Load-test listener chains under peak traffic assumptions