Node.js 14 🟢 The EventEmitter Class and Custom Event Handlers
The EventEmitter class is the backbone of Node.js’s event-driven architecture. It provides a simple mechanism: objects emit named events, and listeners subscribe to those events and run when they are emitted. The http.Server, net.Socket, fs.ReadStream, and process objects are all EventEmitter instances, and the pattern is available to any code that extends the class. Understanding EventEmitter is understanding how Node.js components communicate, how streams signal completion and failure, and how to build custom components that fit the ecosystem’s conventions.
The class is deliberately minimal. It has methods for adding listeners (on, once, addListener), removing them (off, removeListener, removeAllListeners), emitting events (emit), and inspecting them (listeners, listenerCount, eventNames). The power comes from the conventions around it: the error event is special, the newListener and removeListener events fire on subscription changes, and the once method provides a self-removing listener. These details matter because misusing them causes leaks, unhandled errors, and memory growth.
This chapter covers the EventEmitter class, adding and removing listeners, emitting events, the error event and its special behavior, once versus on, listener limits and memory leaks, and the patterns for building custom event emitters.
Key point: EventEmitter lets objects emit named events and lets listeners subscribe to them. The on method adds a persistent listener, and once adds a listener that removes itself after one call. The error event is special: if emitted with no listener, it throws and crashes the process. Always handle the error event when using streams, sockets, and servers. Use removeListener or off to clean up listeners and prevent memory leaks.
Why EventEmitter exists
The decoupling problem. A component that calls a callback directly is coupled to that callback. A component that emits an event is decoupled from its listeners: it announces what happened, and whoever is interested subscribes. This is the observer pattern, and it is central to Node.js’s design.
The multiple-listener problem. A callback supports one receiver. An event supports any number of listeners, each of which runs when the event fires. This is what makes streams and servers composable: a single data event can have several subscribers.
The naming problem. Events have names. A socket emits connect, data, end, error, and close. A stream emits data, end, error, close, and readable. Named events are self-documenting: the name describes what happened, and the listener describes what to do.
The lifecycle problem. Listeners can be added and removed at runtime. The once method removes itself after firing, which is the pattern for one-time events like connect and load. The removeListener method allows explicit cleanup, which matters for long-running processes that subscribe and unsubscribe repeatedly.
The ecosystem problem. The EventEmitter is a standard interface. Any object that extends it can be used wherever an emitter is expected, and tools that work with emitters work with all of them. This uniformity is why the http, net, fs, stream, and process modules all share the same event API.
a. Creating an EventEmitter
The events module exports the EventEmitter class. An instance is created with new, and a custom class extends it with extends.
const { EventEmitter } = require('events');
const emitter = new EventEmitter();
emitter.on('greeting', (name) => {
console.log(`Hello, ${name}!`);
});
emitter.emit('greeting', 'Alice'); // "Hello, Alice!"
A custom class extends EventEmitter to become an emitter itself:
const { EventEmitter } = require('events');
class Job extends EventEmitter {
constructor(name) {
super();
this.name = name;
}
start() {
this.emit('started', this.name);
setTimeout(() => {
this.emit('completed', this.name, 42);
}, 100);
}
}
const job = new Job('import');
job.on('started', (name) => console.log(`${name} started`));
job.on('completed', (name, result) => console.log(`${name} done: ${result}`));
job.start();
The super() call initializes the EventEmitter internals, and the subclass gets on, emit, and the rest of the API.
b. Adding listeners
The on method adds a listener that stays subscribed until it is removed. The once method adds a listener that removes itself after the first invocation.
emitter.on('data', (chunk) => {
console.log('chunk:', chunk); // runs every time
});
emitter.once('connect', () => {
console.log('connected'); // runs once
});
The addListener method is an alias for on:
emitter.addListener('data', handler); // same as emitter.on
Listeners are called in the order they were added:
emitter.on('event', () => console.log('first'));
emitter.on('event', () => console.log('second'));
emitter.emit('event');
// first
// second
The on method returns the emitter, so calls can be chained:
emitter
.on('start', onStart)
.on('data', onData)
.on('end', onEnd);
c. Emitting events
The emit method fires an event and invokes every listener subscribed to it. Arguments after the event name are passed to the listeners.
emitter.emit('greeting', 'Alice', 'Bob');
The listeners receive the arguments in order:
emitter.on('greeting', (a, b) => {
console.log(a, b); // Alice Bob
});
emit returns true if there were listeners for the event and false otherwise.
const hadListeners = emitter.emit('greeting', 'Alice');
console.log(hadListeners); // true or false
This return value is useful for checking whether an event was handled.
Emitting an event synchronously invokes the listeners. If a listener throws, the error propagates to the caller of emit, and the remaining listeners do not run. This is why listeners should handle their own errors or be wrapped in try/catch when the event is emitted in a critical path.
d. The error event
The error event is special. If an EventEmitter emits error and there are no listeners for it, the emitter throws the error and the process crashes. This is deliberate: it surfaces silent failures that would otherwise be lost.
emitter.emit('error', new Error('something failed'));
// Throws if there is no 'error' listener
The fix is always to subscribe to error:
emitter.on('error', (err) => {
console.error('Error:', err.message);
});
This applies to every emitter in the standard library. A net.Socket, an http.Server, a fs.ReadStream, and any custom emitter that can fail should have an error listener. Without one, the first error crashes the process.
The convention extends to functions that take a callback: if an error occurs asynchronously, the callback receives it as the first argument. For emitters, the error is emitted as an error event. The two conventions are parallel: both make errors explicit and impossible to ignore.
A subclass that emits error should document it and ensure callers know to subscribe:
class Parser extends EventEmitter {
parse(input) {
if (!input) {
this.emit('error', new Error('empty input'));
return;
}
// ...
}
}
e. Removing listeners
The off method (an alias for removeListener) removes a specific listener. The removeAllListeners method removes all listeners for an event or all events.
function onData(chunk) {
console.log(chunk);
}
emitter.on('data', onData);
emitter.off('data', onData); // remove the listener
Removing a listener requires a reference to the same function. An inline arrow function cannot be removed because the reference is lost:
emitter.on('data', () => console.log('data'));
// Cannot remove this listener; no reference to the function
The fix is to define the listener as a named function:
function handleData(chunk) {
console.log(chunk);
}
emitter.on('data', handleData);
emitter.off('data', handleData); // works
removeAllListeners is useful for cleanup:
emitter.removeAllListeners('data'); // remove all 'data' listeners
emitter.removeAllListeners(); // remove all listeners
Removing listeners is important in long-running processes. A listener that is added repeatedly without being removed accumulates, which is the classic memory leak in event-driven code.
f. Listener limits and memory leaks
The EventEmitter warns when more than ten listeners are added to a single event. The warning is a heuristic: it usually indicates a leak, where listeners are added but never removed.
MaxListenersExceededWarning: Possible EventEmitter memory leak detected.
11 data listeners added to [EventEmitter]. Use emitter.setMaxListeners() to increase limit.
The limit can be changed with setMaxListeners:
emitter.setMaxListeners(20); // allow up to 20 listeners
emitter.setMaxListeners(0); // unlimited
The getMaxListeners method returns the current limit, and listenerCount returns the number of listeners for an event:
console.log(emitter.getMaxListeners());
console.log(emitter.listenerCount('data'));
The warning is not an error; the listeners are still added. But it is a signal to inspect the code for a leak. The common causes are:
| Cause | Fix |
|---|---|
| Listener added in a loop | Add once, outside the loop |
| Listener added per request | Remove after the request |
| Listener added to a shared emitter | Use once or remove explicitly |
| Interval or timer not cleared | Clear on cleanup |
The listeners method returns an array of the listeners for an event, and eventNames returns the names of events that have listeners:
console.log(emitter.eventNames()); // ['data', 'end']
g. The newListener and removeListener events
The EventEmitter emits newListener before a listener is added and removeListener after a listener is removed. These events are useful for tracking subscriptions, but they have subtleties.
emitter.on('newListener', (event, listener) => {
console.log(`Listener added for ${event}`);
});
emitter.on('data', () => {}); // triggers 'newListener'
The newListener event fires before the listener is added, so listenerCount does not yet include the new listener. The removeListener event fires after the listener is removed.
These events are rarely used in application code, but they are part of the API and are sometimes used by libraries to track or log subscriptions.
Complete Example Session
// ============================================
// PART 1: BASIC EMITTER
// ============================================
const { EventEmitter } = require('events');
const emitter = new EventEmitter();
emitter.on('greeting', (name) => {
console.log(`Hello, ${name}!`);
});
emitter.emit('greeting', 'Alice'); // "Hello, Alice!"
// ============================================
// PART 2: CUSTOM CLASS EXTENDING EMITTER
// ============================================
class Job extends EventEmitter {
constructor(name) {
super();
this.name = name;
}
start() {
this.emit('started', this.name);
setTimeout(() => this.emit('completed', this.name, 42), 100);
}
}
const job = new Job('import');
job.on('started', (name) => console.log(`${name} started`));
job.on('completed', (name, result) => console.log(`${name} done: ${result}`));
job.start();
// ============================================
// PART 3: ONCE VS ON
// ============================================
emitter.on('data', () => console.log('on: runs every time'));
emitter.once('connect', () => console.log('once: runs once'));
emitter.emit('data');
emitter.emit('data');
emitter.emit('connect');
emitter.emit('connect');
// on: runs every time
// on: runs every time
// once: runs once
// ============================================
// PART 4: MULTIPLE LISTENERS
// ============================================
emitter.on('event', () => console.log('first'));
emitter.on('event', () => console.log('second'));
emitter.emit('event');
// first
// second
// ============================================
// PART 5: EMIT RETURN VALUE
// ============================================
const hadListeners = emitter.emit('greeting', 'Alice');
console.log(hadListeners); // true
const noListeners = emitter.emit('unknown');
console.log(noListeners); // false
// ============================================
// PART 6: ERROR EVENT WITHOUT LISTENER
// ============================================
// This would throw and crash:
// emitter.emit('error', new Error('boom'));
emitter.on('error', (err) => {
console.error('Caught:', err.message);
});
emitter.emit('error', new Error('boom')); // Caught: boom
// ============================================
// PART 7: REMOVING LISTENERS
// ============================================
function handleData(chunk) {
console.log('chunk:', chunk);
}
emitter.on('data', handleData);
emitter.off('data', handleData); // removed
// ============================================
// PART 8: LISTENER COUNT
// ============================================
console.log(emitter.listenerCount('event')); // 2
console.log(emitter.eventNames()); // ['event', ...]
// ============================================
// PART 9: MAX LISTENERS WARNING
// ============================================
emitter.setMaxListeners(20);
for (let i = 0; i < 15; i++) {
emitter.on('data', () => {});
}
// No warning because the limit is 20
// ============================================
// PART 10: STREAMS ARE EMITTERS
// ============================================
const fs = require('fs');
const stream = fs.createReadStream('large.txt', 'utf8');
stream.on('data', (chunk) => process.stdout.write('.'));
stream.on('end', () => console.log('\ndone'));
stream.on('error', (err) => console.error('stream error:', err));
These ten parts cover a basic emitter, a custom class extending EventEmitter, once versus on, multiple listeners, the emit return value, the error event, removing listeners, listener count, the max listeners warning, and streams as emitters.
Quick Reference
Methods
| Method | Purpose |
|---|---|
on(event, listener) | Add a persistent listener |
once(event, listener) | Add a one-time listener |
off(event, listener) | Remove a listener |
addListener(event, listener) | Alias for on |
removeListener(event, listener) | Alias for off |
removeAllListeners([event]) | Remove all listeners |
emit(event, ...args) | Fire an event |
listenerCount(event) | Number of listeners |
listeners(event) | Array of listeners |
eventNames() | Names of events with listeners |
setMaxListeners(n) | Set the listener limit |
getMaxListeners() | Get the listener limit |
Special Events
| Event | When it fires |
|---|---|
newListener | Before a listener is added |
removeListener | After a listener is removed |
error | Emitted with an error; throws if no listener |
on vs once
| Aspect | on | once |
|---|---|---|
| Fires | Every time | Once |
| Removes itself | No | Yes |
| Use for | Recurring events | One-time events |
Listener Limit
| Setting | Effect |
|---|---|
| Default | 10 listeners per event |
setMaxListeners(n) | Change the limit |
setMaxListeners(0) | Unlimited |
| Warning | MaxListenersExceededWarning |
Common Emitters
| Emitter | Events |
|---|---|
http.Server | request, listening, error |
net.Socket | connect, data, end, error, close |
fs.ReadStream | data, end, error, close |
process | exit, uncaughtException, SIGINT |
ChildProcess | exit, close, error, message |
Best Practices
✅ Do This:
// Always handle 'error'
stream.on('error', (err) => console.error(err));
// Use once for one-time events
emitter.once('connect', () => console.log('connected'));
// Use named functions for removable listeners
function onData(chunk) { console.log(chunk); }
emitter.on('data', onData);
emitter.off('data', onData);
// Remove listeners on cleanup
emitter.removeAllListeners();
// Extend EventEmitter for custom components
class Worker extends EventEmitter { }
❌ Don’t Do This:
// Ignore the error event
stream.on('data', handler); // ❌ no error handler
// Use inline functions for removable listeners
emitter.on('data', () => {}); // ❌ cannot remove
// Add listeners in a loop without removing
for (let i = 0; i < 100; i++) {
emitter.on('data', handler); // ❌ leak
}
// Emit 'error' without a listener
emitter.emit('error', new Error('boom')); // ❌ crashes
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Process crashes on error | No error listener | Add on('error', ...) |
| Cannot remove listener | Inline function | Use a named function |
| Memory leak warning | Listeners added in a loop | Add once or remove |
| Listener runs multiple times | Used on instead of once | Use once |
| Listeners run in unexpected order | Assumed order | Listeners run in add order |
| Event emitted with no listener | Silent no-op | Check emit return value |
Real-World Examples
1. Basic Emitter
const emitter = new EventEmitter();
emitter.on('event', handler);
emitter.emit('event', arg);
2. Custom Class
class Downloader extends EventEmitter {
start() {
this.emit('start');
// ...
this.emit('done');
}
}
3. Error Handling
stream.on('error', (err) => console.error(err));
4. Once for Connect
socket.once('connect', () => console.log('connected'));
5. Remove Listener
function handler(data) { }
emitter.on('data', handler);
emitter.off('data', handler);
6. Remove All
emitter.removeAllListeners('data');
7. Listener Count
console.log(emitter.listenerCount('data'));
8. Increase Limit
emitter.setMaxListeners(50);
9. Check Emit Result
if (!emitter.emit('event')) {
console.log('no listeners');
}
10. Process Events
process.on('SIGINT', () => {
console.log('shutting down');
process.exit(0);
});
Visual
EventEmitter Flow
┌──────────────────────────────────────────────────────────────┐
│ EMITTER │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ emit('data', chunk) │ │
│ └────────────────────────┬───────────────────────────────┘ │
│ │ │
│ ▼ │
│ LISTENERS │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ listener 1 (on) → runs every time │ │
│ │ listener 2 (on) → runs every time │ │
│ │ listener 3 (once) → runs once, then removed │ │
│ └────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
on vs once
┌──────────────────────────────────────────────────────────────┐
│ emitter.on('event', handler) │
│ ├── emit 1 → handler runs │
│ ├── emit 2 → handler runs │
│ └── emit 3 → handler runs │
│ │
│ emitter.once('event', handler) │
│ ├── emit 1 → handler runs │
│ ├── emit 2 → (listener removed) │
│ └── emit 3 → (listener removed) │
└──────────────────────────────────────────────────────────────┘
Error Event
┌──────────────────────────────────────────────────────────────┐
│ NO ERROR LISTENER: │
│ emitter.emit('error', new Error('boom')) │
│ └── Throws, process crashes │
│ │
│ WITH ERROR LISTENER: │
│ emitter.on('error', (err) => console.error(err)) │
│ emitter.emit('error', new Error('boom')) │
│ └── Listener runs, process continues │
└──────────────────────────────────────────────────────────────┘
Memory Leak from Listeners
┌──────────────────────────────────────────────────────────────┐
│ BAD: │
│ function handleRequest() { │
│ emitter.on('data', handler); // ❌ added per request │
│ } │
│ └── Listeners accumulate │
│ │
│ GOOD: │
│ emitter.on('data', handler); // added once at startup │
│ │
│ OR: │
│ function handleRequest() { │
│ emitter.once('data', handler); // removed after firing │
│ } │
└──────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
EventEmitter | Class for emitting and listening to events |
on | Persistent listener |
once | One-time listener |
off / removeListener | Remove a listener |
removeAllListeners | Remove all listeners |
emit | Fire an event |
error event | Throws if no listener |
| Listener limit | 10 per event by default |
setMaxListeners | Change the limit |
listenerCount | Number of listeners |
eventNames | Names of events with listeners |
| Streams | Are EventEmitter instances |
Key takeaways:
EventEmitteris the standard mechanism for events in Node.js. Objects emit named events, and listeners subscribe. The pattern decouples the emitter from the listeners and supports multiple subscribers.- The
errorevent is special. If emitted with no listener, it throws and crashes the process. Always subscribe toerroron streams, sockets, servers, and any emitter that can fail. onadds a persistent listener;onceadds a one-time listener.onceremoves itself after firing, which is the pattern for one-time events likeconnectandload.- Removing a listener requires a reference to the same function. Inline arrow functions cannot be removed because the reference is lost. Use named functions for listeners that need to be removed.
- The max listeners warning signals a possible leak. More than ten listeners on a single event triggers a warning. The common causes are adding listeners in a loop or per request without removing them.
emitreturns a boolean. It returnstrueif there were listeners for the event andfalseotherwise. This is useful for checking whether an event was handled.- Streams, sockets, and servers are emitters. The
http.Server,net.Socket,fs.ReadStream, andprocessobjects all extendEventEmitter, and their events follow the same conventions.
Remember: The EventEmitter is the backbone of Node.js’s event-driven architecture. It provides a simple, uniform mechanism for objects to announce events and for listeners to subscribe. The error event is the one that must always be handled, because an unhandled error event crashes the process. The once method is the pattern for one-time events, and removeListener is the pattern for cleanup. The max listeners warning is a signal to inspect for leaks. Understanding the EventEmitter is understanding how Node.js components communicate, how streams signal completion and failure, and how to build custom components that fit the ecosystem’s conventions.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!