The events module and the EventEmitter class are the backbone of Node.js. Almost every core module — http, fs, stream, net, child_process — inherits from EventEmitter. It is the Observer pattern built directly into the runtime. Understanding EventEmitter means you understand how Node.js servers receive requests, how streams signal data arrival, how databases signal query completion, and how you can build your own loosely-coupled, event-driven systems. In the MEAN Stack, it is the foundation for Socket.io real-time events, Express middleware, and Node.js stream-based file handling.
EventEmitter API
| Method | Purpose |
|---|---|
emitter.on(event, listener) |
Register a persistent listener — called every time event fires |
emitter.once(event, listener) |
Register a one-time listener — auto-removed after first call |
emitter.off(event, listener) |
Remove a specific listener — alias for removeListener |
emitter.emit(event, ...args) |
Fire event synchronously — all listeners called immediately |
emitter.removeAllListeners(event?) |
Remove all listeners for an event (or all events) |
emitter.listeners(event) |
Returns array of listener functions for an event |
emitter.listenerCount(event) |
Returns number of listeners for an event |
emitter.setMaxListeners(n) |
Change the warning threshold (default: 10) |
emitter.eventNames() |
Returns array of registered event names |
Important Behaviours
| Behaviour | Detail |
|---|---|
| Synchronous emission | emit() calls all listeners synchronously in registration order |
| Error events are special | If 'error' is emitted with no listener, Node.js throws and crashes |
| Memory leak warning | Adding more than 10 listeners to one event triggers a warning |
newListener event |
Emitted before a listener is added — useful for introspection |
removeListener event |
Emitted after a listener is removed |
emit() is synchronous — it calls all registered listeners immediately, in the order they were added, before returning. This is different from DOM events where dispatch is asynchronous in some contexts. If you need async listeners, you must manually make them async: emitter.on('event', async () => { await doSomething(); }) — but be aware that the emitter does not wait for the Promise to resolve.emitter.off('data', () => handler()) will fail to remove the listener because it is a different function object than the one that was registered. Store the listener in a variable: const listener = () => handler(); emitter.on('data', listener); emitter.off('data', listener);'error' event listener to every EventEmitter. Node.js treats an unhandled 'error' event as a fatal error — it throws the error object, which will crash your process if not caught by a top-level handler. This is especially important for stream and socket objects: stream.on('error', err => console.error(err)).Basic Example
const EventEmitter = require('events');
// ── Creating a custom EventEmitter ────────────────────────────────────────
class OrderProcessor extends EventEmitter {
async process(order) {
this.emit('processing', order);
try {
const result = await this.#validateOrder(order);
this.emit('validated', result);
const payment = await this.#chargePayment(result);
this.emit('paid', payment);
await this.#fulfillOrder(payment);
this.emit('fulfilled', { orderId: order.id, status: 'complete' });
} catch (err) {
this.emit('error', err); // MUST be handled — otherwise Node.js crashes
}
}
async #validateOrder(order) { /* ... */ return order; }
async #chargePayment(order) { /* ... */ return order; }
async #fulfillOrder(order) { /* ... */ }
}
const processor = new OrderProcessor();
// Register listeners
processor.on('processing', order => console.log('Processing order:', order.id));
processor.on('validated', order => console.log('Order validated:', order.id));
processor.on('paid', payment=> console.log('Payment received:', payment.id));
processor.on('fulfilled', result => console.log('Order complete:', result.orderId));
processor.on('error', err => console.error('Order failed:', err.message));
// Process an order
processor.process({ id: 'ORD-001', total: 99.99 });
// ── on vs once ────────────────────────────────────────────────────────────
const emitter = new EventEmitter();
emitter.on('data', chunk => console.log('on — fires every time:', chunk));
emitter.once('connect', () => console.log('once — fires only the first time'));
emitter.emit('data', 'chunk 1'); // on fires
emitter.emit('data', 'chunk 2'); // on fires
emitter.emit('connect'); // once fires
emitter.emit('connect'); // once does NOT fire again
// ── Removing listeners ────────────────────────────────────────────────────
const emitter2 = new EventEmitter();
const handleData = (data) => console.log('Received:', data);
emitter2.on('data', handleData);
emitter2.emit('data', 'hello'); // fires
emitter2.off('data', handleData); // remove it
emitter2.emit('data', 'hello'); // does NOT fire — listener removed
// ── Listener count and memory leak prevention ─────────────────────────────
const emitter3 = new EventEmitter();
emitter3.setMaxListeners(20); // suppress warning for expected high counts
for (let i = 0; i < 15; i++) {
emitter3.on('message', msg => console.log(i, msg));
}
console.log(emitter3.listenerCount('message')); // 15
console.log(emitter3.eventNames()); // ['message']
// ── Error event handling ──────────────────────────────────────────────────
const safeEmitter = new EventEmitter();
// ALWAYS register an error listener
safeEmitter.on('error', (err) => {
console.error('Handled error:', err.message);
// process continues
});
safeEmitter.emit('error', new Error('Something went wrong'));
// Without the listener above — Node.js would throw and crash
Real-World Example: Application Event Bus
// event-bus.js — application-wide event bus (Singleton)
const EventEmitter = require('events');
class EventBus extends EventEmitter {
constructor() {
super();
this.setMaxListeners(50);
// Always handle errors
this.on('error', err => {
console.error('[EventBus] Unhandled error:', err);
});
}
// Type-safe emit with logging
publish(event, payload) {
console.log(`[EventBus] Publishing: ${event}`, payload);
this.emit(event, payload);
}
// Subscribe with automatic cleanup on 'destroy'
subscribe(event, listener, { once = false } = {}) {
once ? this.once(event, listener) : this.on(event, listener);
return () => this.off(event, listener); // return unsubscribe fn
}
}
// Export singleton
module.exports = new EventBus();
// ── Usage across modules ───────────────────────────────────────────────────
// auth.controller.js
const bus = require('./event-bus');
async function login(req, res) {
const user = await AuthService.login(req.body);
bus.publish('user.login', { userId: user.id, ip: req.ip, ts: Date.now() });
res.json({ token: generateToken(user) });
}
// audit.service.js — reacts to login events
const bus = require('./event-bus');
const unsubscribe = bus.subscribe('user.login', async ({ userId, ip, ts }) => {
await AuditLog.create({ userId, event: 'login', ip, timestamp: ts });
console.log(`[Audit] User ${userId} logged in from ${ip}`);
});
// notification.service.js — also reacts to login
bus.subscribe('user.login', ({ userId }) => {
console.log(`[Notifications] Checking alerts for user ${userId}`);
});
// Clean up on process shutdown
process.on('SIGTERM', () => {
unsubscribe();
bus.removeAllListeners();
});
Common Mistakes
Mistake 1 — Using anonymous arrow functions that cannot be removed
❌ Wrong — cannot call off() with an anonymous function:
emitter.on('data', (d) => process(d));
// Later:
emitter.off('data', (d) => process(d)); // DOES NOT WORK — different function object
✅ Correct — store a reference to remove it later:
const handler = (d) => process(d);
emitter.on('data', handler);
// Later:
emitter.off('data', handler); // works — same reference
Mistake 2 — Not handling the error event
❌ Wrong — crashes the Node.js process:
const stream = fs.createReadStream('./nonexistent-file.txt');
stream.on('data', chunk => process(chunk));
// stream emits 'error' — no listener — Node.js THROWS and process crashes
✅ Correct — always add an error listener to every EventEmitter:
const stream = fs.createReadStream('./file.txt');
stream.on('data', chunk => process(chunk));
stream.on('error', err => console.error('Stream error:', err.message));
stream.on('end', () => console.log('Done'));
Mistake 3 — Async listeners where errors are swallowed silently
❌ Wrong — async listener error is swallowed, emitter never knows it failed:
emitter.on('order', async (order) => {
await processOrder(order); // if this throws — unhandled rejection!
});
✅ Correct — catch errors inside async listeners and emit or log them:
emitter.on('order', async (order) => {
try {
await processOrder(order);
} catch (err) {
emitter.emit('error', err); // propagate to error listener
}
});
Quick Reference
| Task | Code |
|---|---|
| Create custom emitter | class MyEmitter extends EventEmitter {} |
| Listen persistently | emitter.on('event', handler) |
| Listen once | emitter.once('event', handler) |
| Remove listener | emitter.off('event', handler) |
| Fire event | emitter.emit('event', payload) |
| Count listeners | emitter.listenerCount('event') |
| List event names | emitter.eventNames() |
| Prevent leak warning | emitter.setMaxListeners(20) |
| Remove all listeners | emitter.removeAllListeners('event') |