Node.js is often described as event-driven, and the events system is one of the reasons the platform feels so natural for servers, streams, real-time applications, and asynchronous workflows. Instead of constantly checking whether something has happened, code can register interest in an event and respond when that event is emitted. This style is based on the observer pattern, where one part of the program emits notifications and another part listens for them.
The built-in events module provides the EventEmitter class, which is the foundation of many Node.js core features. Streams, HTTP servers, file operations, process events, and many third-party libraries rely on events internally. Understanding EventEmitter helps you understand not only custom events you write yourself, but also how many important Node.js APIs are designed.
A simple event system usually follows three steps:
When the event is emitted, Node.js immediately calls the listeners that were registered for that event. This gives your code a structured way to react to actions such as user registration, file upload completion, payment success, or data loading.
To use custom events in Node.js, start by importing the events module and creating an EventEmitter instance.
After creating the emitter, you can attach named listeners and emit events whenever your program reaches the appropriate point.
const EventEmitter = require("events");
const emitter = new EventEmitter();
The on() method is used to bind a listener to an event name. Every time that event is emitted, the listener runs.
Event names are just strings. Good event names usually describe something that happened, such as userCreated, paymentSuccess, fileUploaded, or orderCancelled.
emitter.on("welcome", () => {
console.log("Welcome event received");
});
The emit() method triggers the event and runs all listeners attached to that event name.
If a listener has been registered for welcome, it runs when the event is emitted. If no listener exists, nothing visible happens. This is one reason events are flexible: the emitter does not need to know which specific actions will respond.
emitter.emit("welcome");
A good beginner example is a registration workflow. After a user is registered, one part of the app might welcome the user, another might log the event, and another might send an email. Events help separate those reactions from the core registration logic.
This example also shows that one event can have multiple listeners. When registration is emitted, all listeners for that event run in the order they were registered.
const EventEmitter = require("events");
const emitter = new EventEmitter();
emitter.on("registration", username => {
console.log(`Registration successful for ${username}`);
});
emitter.on("registration", username => {
console.log(`Sending welcome email to ${username}`);
});
emitter.emit("registration", "Aman");
Events often need to carry information, such as a username, file name, status object, or error message. The emit() method allows you to pass arguments after the event name, and listeners receive those values as parameters.
Passing data this way makes events more useful and avoids the need to rely on shared global state.
emitter.on("orderPlaced", (orderId, amount) => {
console.log(`Order ${orderId} placed for amount ${amount}`);
});
emitter.emit("orderPlaced", "ORD-101", 499.99);
The EventEmitter API has several useful methods beyond on() and emit().
| Method | Description |
|---|---|
| on(event, listener) | Registers a listener that runs every time the event is emitted. |
| once(event, listener) | Registers a listener that runs only the first time the event occurs. |
| emit(event, ...args) | Triggers the event and passes any arguments to listeners. |
| off(event, listener) | Removes a specific listener. |
| removeAllListeners(event) | Removes all listeners for the event. |
| listenerCount(event) | Returns how many listeners are attached to an event. |
Sometimes an event should be handled only one time. For example, you may want a connection-ready listener to fire only when the first successful connection happens. In these cases, once() is a better choice than on().
This is useful when a response should happen only the first time an event is observed.
const EventEmitter = require("events");
const emitter = new EventEmitter();
emitter.once("connected", () => {
console.log("Connected successfully");
});
emitter.emit("connected"); // runs
emitter.emit("connected"); // ignored
A powerful pattern in Node.js is to create your own class that extends EventEmitter. This allows the class to emit meaningful events as part of its normal behavior. It is a clean way to add event-driven capabilities to your own code.
This pattern is helpful when building services, managers, workers, or utility classes that need to notify other parts of the application about progress, success, failure, or state changes.
const EventEmitter = require("events");
class DownloadManager extends EventEmitter {
startDownload(fileName) {
console.log(`Starting download for ${fileName}...`);
setTimeout(() => {
this.emit("completed", fileName);
}, 1000);
}
}
const manager = new DownloadManager();
manager.on("completed", fileName => {
console.log(`Download completed for ${fileName}`);
});
manager.startDownload("report.pdf");
It is important to separate two related but different ideas: events and asynchronous operations. Events are a notification mechanism. Asynchronous operations are tasks that complete later. Often, asynchronous code emits events when work progresses or finishes, but the two concepts are not identical. For example, a stream may emit a data event every time a chunk arrives, and an end event when the stream is finished. The asynchronous file or network operation is the underlying process, while the emitted events are how your code is informed about what happened.
This is why events fit so naturally into Node.js. Instead of blocking while waiting for slow work to finish, Node.js can continue doing other things and fire the relevant listeners at the right moment.
Events appear in many built-in Node.js APIs:
This means that learning custom EventEmitter code is not isolated knowledge. It helps you understand the shape of many other Node.js APIs as well.
In event-driven code, errors should be handled carefully. A common Node.js convention is to emit an error event when something goes wrong. If an emitter uses error events, listeners should usually be attached so failures are not ignored.
In many Node.js scenarios, failing to handle an important error event can cause the application to crash or behave unpredictably.
const EventEmitter = require("events");
const emitter = new EventEmitter();
emitter.on("error", error => {
console.error("Something went wrong:", error.message);
});
emitter.emit("error", new Error("Database connection failed"));
If listeners are no longer needed, they should be removed. This helps avoid memory leaks or repeated handlers that continue reacting after their work is done.
Node.js also warns when too many listeners are added to the same event, because that can be a sign of leaking listeners over time.
function logMessage(message) {
console.log(message);
}
emitter.on("message", logMessage);
emitter.emit("message", "First call");
emitter.off("message", logMessage);
emitter.emit("message", "Second call"); // no output
One common mistake is thinking events themselves are asynchronous by default. In reality, emit() calls listeners synchronously in the order they were registered. Another mistake is registering listeners repeatedly inside loops or request handlers without removing them, which can lead to duplicate output and memory leak warnings. Beginners also sometimes expect an emitted event to "wait" for async code inside a listener, but that is not how EventEmitter works. If the listener starts asynchronous work, that work continues separately after the listener returns.
Another common issue is choosing unclear event names. Names like doStuff are vague, while names like userRegistered, fileSaved, or paymentFailed clearly communicate what happened. Good event naming makes event-driven systems much easier to follow.
Think of events as signals. One part of your application raises the signal when something meaningful happens, and any interested listeners respond. This allows the emitter to stay focused on its own task while other code reacts in its own way. That separation is one of the biggest strengths of event-driven design in Node.js.
Node warns when many listeners are attached to the same event because it often means code is adding a listener repeatedly without removing it. For example, attaching a new listener inside every request can accumulate handlers for the lifetime of the process.
The process can throw and crash. In Node, the error event is special: if no listener handles it, the error is treated as an unhandled exception.
Use once when only the first occurrence matters, such as an initialization event, a single response, or a completion signal. The listener removes itself after running, which reduces cleanup work and avoids accidental repeated behavior.
Explore 500+ free tutorials across 20+ languages and frameworks.