1. Introduction 🔔
Node.js is built around events — from HTTP requests to stream chunks, much of the platform's internal machinery is powered by a single class: EventEmitter. This tutorial explores how events work, and how to build your own event-driven APIs.
2. What are Events? 📡
An event is a signal that something happened — a button was clicked, a file finished downloading, a connection closed. Instead of constantly checking whether something occurred, code subscribes to an event and reacts only when it's emitted.
- Decouples the code that detects something from the code that reacts to it.
- Supports multiple independent listeners for the same event.
- Forms the backbone of Node's http, stream, and net modules.
3. Event-Driven Architecture 🏛️
In an event-driven architecture, the flow of a program is determined largely by events — user input, sensor output, or messages from other programs — rather than a fixed, linear sequence of instructions.
4. The events Module 📦
Node's built-in events module (node:events) exports the EventEmitter class — the foundation of virtually all event-based APIs in the platform.
events-import.js
const EventEmitter = require('node:events');5. EventEmitter 🎛️
EventEmitter is a class implementing the observer pattern: objects (emitters) maintain a list of listeners per event name and invoke them, synchronously and in registration order, whenever that event is emitted.
5.1 Creating an Event Emitter
create-emitter.js
const EventEmitter = require('node:events');
const emitter = new EventEmitter();5.2 Registering Event Listeners
register-listener.js
emitter.on('userLoggedIn', (username) => {
console.log(`${username} just logged in!`);
});5.3 Emitting Events
emit-event.js
emitter.emit('userLoggedIn', 'alice');
// => "alice just logged in!"Important
5.4 Passing Event Arguments
Any additional arguments passed to emit() are forwarded directly to every registered listener.
event-arguments.js
emitter.on('orderPlaced', (orderId, total, currency) => {
console.log(`Order ${orderId}: ${total} ${currency}`);
});
emitter.emit('orderPlaced', 'ORD-123', 49.99, 'USD');5.5 One-Time Listeners
once() registers a listener that fires exactly once, then automatically removes itself.
once-example.js
emitter.once('ready', () => console.log('Initialized!'));
emitter.emit('ready'); // logs "Initialized!"
emitter.emit('ready'); // nothing happens5.6 Removing Event Listeners
remove-listener.js
function onTick() {
console.log('tick');
}
emitter.on('tick', onTick);
emitter.off('tick', onTick); // or emitter.removeListener('tick', onTick)Caution
5.7 Listener Execution Order
Listeners for a given event run in the order they were registered, synchronously, one after another.
5.8 Error Events
The special 'error' event is treated differently: if emitted without a registered listener, Node throws the error and, by default, crashes the process.
error-event.js
emitter.on('error', (err) => {
console.error('Something went wrong:', err.message);
});
emitter.emit('error', new Error('Connection failed'));Danger
6. Custom Events 🎨
6.1 Event Names
Event names are just strings (or Symbols) — there's no predefined registry. By convention, use camelCase names that describe what happened, like 'dataReceived' or 'connectionClosed'.
6.2 Multiple Listeners
A single event can have many listeners; all of them fire when the event is emitted.
multiple-listeners.js
emitter.on('fileUploaded', (name) => console.log(`Logging upload: ${name}`));
emitter.on('fileUploaded', (name) => console.log(`Sending notification for: ${name}`));
emitter.emit('fileUploaded', 'report.pdf');7. Asynchronous Event Handling ⏱️
Listeners themselves can be async, but emit() does not wait for them to resolve — it fires all listeners and returns immediately, regardless of whether they're synchronous or asynchronous.
async-listener.js
emitter.on('save', async (data) => {
await saveToDatabase(data); // emit() won't wait for this
});
emitter.emit('save', { id: 1 });
console.log('This logs before saveToDatabase necessarily finishes');8. Memory Leak Warnings ⚠️
By default, Node warns if more than 10 listeners are added for a single event on one emitter — often a sign of a listener leak (e.g. registering inside a loop without ever removing them).
max-listeners.js
emitter.setMaxListeners(20); // raise the threshold if genuinely needed
console.log(emitter.getMaxListeners());Warning
9. EventEmitter Methods 🧰
| Method | Description |
|---|---|
| on(event, listener) | Register a listener for an event |
| once(event, listener) | Register a listener that fires only once |
| off(event, listener) | Remove a specific listener |
| emit(event, ...args) | Trigger an event synchronously |
| removeAllListeners([event]) | Remove all (or all-for-one-event) listeners |
| listenerCount(event) | Number of listeners registered for an event |
| eventNames() | List of event names with active listeners |
10. Extending EventEmitter 🏗️
The most common pattern is to subclass EventEmitter, giving a custom class built-in event capabilities.
extend-emitter.js
const EventEmitter = require('node:events');
class Downloader extends EventEmitter {
start(url) {
this.emit('start', url);
// ... simulate download ...
this.emit('progress', 50);
this.emit('complete', url);
}
}
const downloader = new Downloader();
downloader.on('start', (url) => console.log(`Starting: ${url}`));
downloader.on('progress', (pct) => console.log(`Progress: ${pct}%`));
downloader.on('complete', (url) => console.log(`Done: ${url}`));
downloader.start('https://example.com/file.zip');11. Real-World Use Cases 🌍
12. Performance Considerations ⚡
- Emitting events is cheap and synchronous — avoid heavy synchronous logic directly inside listeners on hot paths.
- Removing unused listeners promptly avoids both memory growth and wasted CPU cycles on dead code paths.
- Prefer once() for events that logically fire a single time, to avoid manual cleanup.
13. Best Practices ✅
- Always attach an 'error' listener to emitters that can emit errors.
- Use descriptive, consistent camelCase event names across your codebase.
- Remove listeners (off) when a subscriber's lifecycle ends, to prevent leaks.
- Keep listener functions small and focused; delegate heavy logic to separate functions.
- Document the events a class emits and their expected argument shapes.
14. Common Mistakes ⚠️
- Forgetting an 'error' listener, causing an unhandled crash on the first emitted error.
- Registering listeners inside loops or repeated function calls, quietly leaking memory.
- Expecting emit() to wait for async listeners to finish before continuing.
- Passing a different function reference to off() than was passed to on(), so the listener never gets removed.
- Overusing events for logic that would be clearer as a direct function call.