The EventEmitter class, on/emit/once/removeListener, custom events, why streams and HTTP are built on EventEmitter, and memory leak prevention.
Module F-9 — Event Emitters & Node's Event-Driven Core
What this module covers: The EventEmitter class is the foundation of Node.js's architecture. HTTP servers, file streams, database connections, WebSockets — they all inherit from EventEmitter. You have already used it without knowing it: every time you called
server.listen()orreq.on('data', ...), you were using an EventEmitter. This module explains how EventEmitter works, how to build your own, how to avoid the memory leak that trips up every developer, and how the pattern underpins Node.js internals. Understanding this bridges Foundation to everything in the Practitioner and Architect phases.
What Is an Event Emitter?
An EventEmitter is an object that:
- Lets you register named listeners — functions to call when a specific event occurs
- Lets you emit events by name — which synchronously calls all registered listeners
It is the observer pattern, built into the Node.js runtime.
An EventEmitter is a radio station, not a phone call — it broadcasts on a named frequency without knowing or caring who's tuned in. A Promise is a phone call — one caller, one answer, then it's over.
import { EventEmitter } from 'node:events';
const emitter = new EventEmitter();
// Register a listener for the 'greet' event
emitter.on('greet', (name) => {
console.log(`Hello, ${name}!`);
});
// Emit the event — calls all registered 'greet' listeners
emitter.emit('greet', 'Jatin'); // → Hello, Jatin!
emitter.emit('greet', 'World'); // → Hello, World!
Simple enough. The power comes from the fact that the emitter and the listener are decoupled — the code that fires the event does not know who is listening, and the listener does not know when or why the event was fired.
Core EventEmitter API
import { EventEmitter } from 'node:events';
const emitter = new EventEmitter();
// on() — listen for every occurrence of an event
emitter.on('data', (chunk) => {
console.log('Received:', chunk);
});
// once() — listen only once, then automatically remove the listener
emitter.once('connected', () => {
console.log('Connected to database');
});
// emit() — fire an event with optional arguments
emitter.emit('data', Buffer.from('hello'));
emitter.emit('connected');
emitter.emit('connected'); // second emit — the 'once' listener is gone
// off() / removeListener() — remove a specific listener
function onError(err) {
console.error('Error:', err.message);
}
emitter.on('error', onError);
emitter.off('error', onError); // remove it later
// removeAllListeners() — remove all listeners for an event (or all events)
emitter.removeAllListeners('data');
emitter.removeAllListeners(); // clear everything
// listenerCount() — how many listeners are registered for an event
console.log(emitter.listenerCount('data')); // 0
// eventNames() — list all events that have listeners
console.log(emitter.eventNames());
Building Your Own EventEmitter Subclass
The real pattern: extend EventEmitter in your own classes. This is how Node.js HTTP servers, streams, and virtually every I/O object are built.
import { EventEmitter } from 'node:events';
class DataPipeline extends EventEmitter {
#isRunning = false;
#processed = 0;
start() {
if (this.#isRunning) return;
this.#isRunning = true;
this.emit('started');
// Simulate processing records
const interval = setInterval(() => {
this.#processed++;
const record = { id: this.#processed, value: Math.random() };
this.emit('record', record); // emit each record
if (this.#processed >= 5) {
clearInterval(interval);
this.#isRunning = false;
this.emit('finished', this.#processed);
}
}, 100);
}
stop() {
this.#isRunning = false;
this.emit('stopped');
}
}
// Usage
const pipeline = new DataPipeline();
pipeline.on('started', () => console.log('Pipeline started'));
pipeline.on('record', (record) => console.log('Processing:', record));
pipeline.on('finished', (count) => console.log(`Done. Processed ${count} records.`));
pipeline.start();
Output:
Pipeline started
Processing: { id: 1, value: 0.4521... }
Processing: { id: 2, value: 0.8734... }
...
Done. Processed 5 records.
The caller does not need to know anything about the pipeline's internals. It just listens for named events.
The error Event Is Special
If an EventEmitter emits an 'error' event and there is no listener registered for it, Node.js throws the error and crashes the process. This is not optional — it is a design decision to force you to handle errors explicitly.
const emitter = new EventEmitter();
// ❌ This will crash the process
emitter.emit('error', new Error('Something went wrong'));
// → Uncaught Error: Something went wrong
// ✅ Always register an error listener
emitter.on('error', (err) => {
console.error('Emitter error:', err.message);
});
emitter.emit('error', new Error('Something went wrong'));
// → Emitter error: Something went wrong
Any class extending EventEmitter should either:
- Register a default
'error'listener in the constructor, or - Document clearly that callers must register one
The Memory Leak Warning
This is the single most common EventEmitter mistake. By default, Node.js warns if you register more than 10 listeners for the same event on the same emitter:
MaxListenersExceededWarning: Possible EventEmitter memory leak detected.
11 'data' listeners added to [EventEmitter].
Sign in to keep reading
The rest of this module is free — sign in with Google to unlock it and track your progress.
Sign in & RegisterDiscussion
0Join the discussion