Difficulty: Intermediate
The EventEmitter class is at the core of Node.js's event-driven architecture. Many built-in Node.js modules like HTTP servers, streams, and the process object are instances of EventEmitter. Understanding EventEmitter is fundamental because it is the pattern used throughout the Node.js ecosystem for handling asynchronous events. You import it from the 'events' module: const { EventEmitter } = require('events').
An EventEmitter instance has methods for registering listener functions (callbacks) for named events and for triggering those events. The on(eventName, listener) method registers a listener that runs every time the named event is emitted. The once(eventName, listener) method registers a listener that runs only the first time the event is emitted, then automatically removes itself. The emit(eventName, ...args) method triggers the event, calling all registered listeners synchronously in the order they were registered, passing any additional arguments to each listener.
Event listeners are called synchronously in the order they were added. This is an important detail: emit() does not schedule callbacks for later execution like setTimeout. The listeners run immediately, blocking the emit call until all listeners complete. If a listener performs heavy synchronous work, it delays all subsequent listeners and the code after emit(). For long-running operations, listeners should delegate to asynchronous APIs or use setImmediate to yield control.
The 'error' event has special behavior in Node.js. If an EventEmitter emits an 'error' event and there are no registered listeners for it, Node.js throws the error as an uncaught exception and the process crashes. This is a safety mechanism to prevent silent failures. Always register an 'error' listener on any EventEmitter, or use a try-catch around emit calls. In production code, you should also listen for 'uncaughtException' and 'unhandledRejection' on the process object as a last resort.
Creating your own event-driven classes is straightforward: extend EventEmitter. This pattern is used extensively in Node.js libraries. For example, you might create a FileWatcher class that emits 'change' events, a JobQueue that emits 'job:complete' events, or a DataStream that emits 'data' and 'end' events. The removeListener(eventName, listener) or off(eventName, listener) method removes a specific listener, and removeAllListeners(eventName) removes all listeners for an event. Use listenerCount(eventName) to check how many listeners are registered.
const { EventEmitter } = require('events');
const emitter = new EventEmitter();
// Register listeners with on()
emitter.on('greet', (name) => {
console.log(`Hello, ${name}!`);
});
emitter.on('greet', (name) => {
console.log(`Welcome aboard, ${name}!`);
});
// Register a one-time listener with once()
emitter.once('connect', () => {
console.log('Connected (fires only once)');
});
// Emit events
emitter.emit('greet', 'Alice');
console.log('---');
emitter.emit('connect');
emitter.emit('connect'); // No output, listener was removed
console.log('---');
// Check listener count
console.log('greet listeners:', emitter.listenerCount('greet'));
console.log('connect listeners:', emitter.listenerCount('connect'));
Multiple listeners can be registered for the same event and run in order. The 'once' listener is automatically removed after the first call, so the second 'connect' emit produces no output. listenerCount shows how many active listeners exist for an event.
const { EventEmitter } = require('events');
const emitter = new EventEmitter();
// The 'error' event is special: if unhandled, it crashes the process
emitter.on('error', (err) => {
console.log('Caught error:', err.message);
});
emitter.emit('error', new Error('Something went wrong'));
// Removing specific listeners
function onData(data) {
console.log('Received:', data);
}
emitter.on('data', onData);
emitter.emit('data', 'first message');
// Remove the listener
emitter.removeListener('data', onData);
// or: emitter.off('data', onData);
emitter.emit('data', 'second message'); // No output
console.log('data listeners after removal:', emitter.listenerCount('data'));
// Remove all listeners for an event
emitter.on('tick', () => console.log('tick 1'));
emitter.on('tick', () => console.log('tick 2'));
console.log('tick listeners:', emitter.listenerCount('tick'));
emitter.removeAllListeners('tick');
console.log('tick listeners after removeAll:', emitter.listenerCount('tick'));
Always handle the 'error' event to prevent crashes. removeListener/off requires a reference to the exact function that was registered (arrow functions cannot be removed unless stored). removeAllListeners clears all listeners for a given event.
const { EventEmitter } = require('events');
class JobQueue extends EventEmitter {
constructor() {
super();
this.jobs = [];
}
addJob(job) {
this.jobs.push(job);
this.emit('job:added', job);
}
processAll() {
while (this.jobs.length > 0) {
const job = this.jobs.shift();
this.emit('job:start', job);
// Simulate processing
const result = `Processed: ${job.name}`;
this.emit('job:complete', job, result);
}
this.emit('queue:empty');
}
}
const queue = new JobQueue();
queue.on('job:added', (job) => console.log(`Added: ${job.name}`));
queue.on('job:start', (job) => console.log(`Starting: ${job.name}`));
queue.on('job:complete', (job, result) => console.log(`Done: ${result}`));
queue.once('queue:empty', () => console.log('All jobs complete!'));
queue.addJob({ name: 'Send emails' });
queue.addJob({ name: 'Generate report' });
console.log('---');
queue.processAll();
Extending EventEmitter lets you create event-driven classes. The JobQueue emits events for each stage of processing, allowing external code to react without tight coupling. This pattern is used by libraries like Express (request/response events) and Mongoose (connection events).
on, emit, once, removeListener, error Event, Custom Emitters