Node.js EventEmitter is a core feature of Node.js that allows different parts of an application to communicate through events. Instead of requiring one part of the program to call another part directly, an object can emit a named event, and any registered event listeners can respond to it. This approach is useful for understanding the Node.js events module, custom events, the Observer pattern, and the event-driven architecture used throughout Node.js.
Table of Contents
1. What Is an Event?
An event is a signal that something has happened.
Consider the kinds of things an application might do:
- A user creates an account.
- A file finishes loading.
- A database operation completes.
- A network connection is established.
- An order is successfully created.
- A stream receives a new chunk of data.
Each situation represents something that has happened and may be relevant to other parts of the application.
For example, when a customer places an order, several actions might need to follow:
- Save the order.
- Send a confirmation email.
- Record an activity log.
- Notify an inventory service.
One approach is to write all those operations directly inside the order-creation function. That can work initially, but it makes the function responsible for more and more unrelated tasks.
An event-driven design separates the original operation from the reactions to it.
Customer places an order
|
v
Order service
|
v
Emit "orderCreated"
|
+----------+----------+
| | |
v v v
Send email Write log Update inventory
The order service announces that an order was created. Other parts of the application decide whether they need to respond.
This is the central idea behind event-driven programming: one part of the program announces an event, and other parts react to it.
An emitted event does not automatically send an email, save a file, or perform any other work. Those reactions happen only when listeners have been registered to perform them.
2. What Is Node.js EventEmitter?
Node.js provides the EventEmitter class through its built-in node:events module. It lets JavaScript objects emit named events and maintain a collection of functions that respond to those events.
You do not need to install a package to use it.
With ECMAScript modules:
import { EventEmitter } from "node:events";
With CommonJS:
const { EventEmitter } = require("node:events");
Both examples access the same built-in Node.js API. Choose the syntax that matches your project’s module system.
An EventEmitter has three important parts:
Emitter: The object responsible for announcing an event.
Event name: A name identifying what happened, such as "orderCreated" or "connected".
Listener: A callback function registered to respond when that event occurs.
A typical interaction looks like this:
Create an EventEmitter
|
v
Register a listener with .on()
|
v
Emit the event with .emit()
|
v
The matching listener function runs
The two methods you will use most often are .on() and .emit().
Let’s see them in action.
3. Your First EventEmitter Example
Create a file named event-emitter.mjs:
import { EventEmitter } from "node:events";
const emitter = new EventEmitter();
// Register an event listener.
emitter.on("greet", () => {
console.log("Hello! An event occurred.");
});
// Emit the event.
emitter.emit("greet");
Run it:
node event-emitter.mjs
Output:
Hello! An event occurred.
Let’s break down the example.
Creating an emitter
const emitter = new EventEmitter();
This creates an instance of the EventEmitter class. It can now register listeners and emit events.
Registering a listener
emitter.on("greet", () => {
console.log("Hello! An event occurred.");
});
The .on() method registers a callback function for the event named "greet".
The callback is the function passed as the second argument. It does not run just because it has been registered.
At this point, Node.js has been told what to do when the event occurs, but the event has not yet been emitted.
Emitting the event
emitter.emit("greet");
The .emit() method announces the "greet" event. Node.js then invokes the listener registered for that event.
Event names are identifiers, not special JavaScript keywords. You could use "login", "dataReceived", "fileSaved", or another meaningful name.
A spelling mismatch matters:
emitter.on("userCreated", () => {
console.log("User created");
});
emitter.emit("usercreated");
These are different event names because the capitalization differs. The output is empty because no listener is registered for "usercreated".
4. Understanding the Main EventEmitter Methods
Node.js EventEmitter provides several methods for managing listeners and events. The most useful ones to learn first are on(), emit(), once(), off(), and removeAllListeners().
on(): Listen every time an event occurs
The .on() method registers a listener that runs whenever the specified event is emitted.
import { EventEmitter } from "node:events";
const emitter = new EventEmitter();
emitter.on("message", () => {
console.log("A message was received");
});
emitter.emit("message");
emitter.emit("message");
emitter.emit("message");
Output:
A message was received
A message was received
A message was received
The listener remains registered after each event. As long as it is registered, each matching emission invokes it.
This is useful for repeated events such as incoming data, connection changes, or application notifications.
emit(): Announce an event
The .emit() method triggers listeners associated with an event name.
It can also pass data to those listeners:
import { EventEmitter } from "node:events";
const emitter = new EventEmitter();
emitter.on("userCreated", (user) => {
console.log(`New user: ${user.name}`);
});
emitter.emit("userCreated", {
id: 101,
name: "Aarav",
});
Output:
New user: Aarav
Here, the object passed to .emit() becomes the argument received by the listener.
You can pass multiple arguments, too:
emitter.on("paymentReceived", (orderId, amount) => {
console.log(`Order ${orderId} received ${amount}`);
});
emitter.emit("paymentReceived", "ORD-42", 1500);
Output:
Order ORD-42 received 1500
The values passed after the event name are forwarded to each matching listener in the same order.
An important detail: .emit() returns a boolean indicating whether any listeners were registered for that event when it was emitted. It does not return a listener’s return value.
For example:
const hadListeners = emitter.emit("paymentReceived", "ORD-43", 900);
console.log(hadListeners);
This prints true if the event had at least one registered listener, and false otherwise.
once(): Listen for an event one time
Sometimes an operation should respond to an event only once.
For example, you might need to run initialization logic the first time a service becomes ready.
Use .once():
import { EventEmitter } from "node:events";
const emitter = new EventEmitter();
emitter.once("ready", () => {
console.log("Initialization complete");
});
emitter.emit("ready");
emitter.emit("ready");
emitter.emit("ready");
Output:
Initialization complete
After the first matching emission, Node.js removes that one-time listener. Later emissions do not invoke it.
Use .on() for repeated events and .once() for one-time events.
off(): Remove a listener
A listener does not always need to remain registered for the lifetime of an emitter.
Suppose you register a listener while a component is active. When that component is no longer needed, you may want to remove the listener to avoid unnecessary work or unwanted references to objects.
Use .off() with the event name and the original callback function:
import { EventEmitter } from "node:events";
const emitter = new EventEmitter();
function handleMessage(message) {
console.log(`Received: ${message}`);
}
emitter.on("message", handleMessage);
emitter.emit("message", "Hello");
emitter.off("message", handleMessage);
emitter.emit("message", "Goodbye");
Output:
Received: Hello
The second emission produces no output because the listener has been removed.
Notice that the callback is stored in a named function:
function handleMessage(message) {
console.log(`Received: ${message}`);
}
This matters because .off() needs a reference to the same function that was registered.
This will not work as expected:
emitter.on("message", (message) => {
console.log(message);
});
emitter.off("message", (message) => {
console.log(message);
});
Although the arrow functions contain identical code, they are two separate function objects. The second function is not the listener that was originally registered.
removeAllListeners(): Remove multiple listeners
You can remove every listener for a particular event:
emitter.removeAllListeners("message");
Or remove every listener from the emitter:
emitter.removeAllListeners();
Use this carefully. Removing listeners from an emitter shared by multiple parts of an application can silently prevent other components from receiving important events.
For ordinary application code, removing the specific listener you own is generally easier to reason about.
5. An EventEmitter Can Have Multiple Listeners
One useful feature of Node.js EventEmitter is that several listeners can respond to the same event.
import { EventEmitter } from "node:events";
const emitter = new EventEmitter();
emitter.on("orderCreated", () => {
console.log("Sending confirmation email");
});
emitter.on("orderCreated", () => {
console.log("Writing order to the activity log");
});
emitter.on("orderCreated", () => {
console.log("Updating inventory");
});
emitter.emit("orderCreated");
Output:
Sending confirmation email
Writing order to the activity log
Updating inventory
All three listeners respond to the same event, in registration order.
This is helpful when several independent tasks need to react to one occurrence. The code that emits the event does not need to know how many listeners exist or what each one does.
Conceptually:
"orderCreated"
|
+-----------+-----------+
| | |
v v v
Listener A Listener B Listener C
| | |
v v v
Email Logs Inventory
This is also where event-driven architecture can make code easier to maintain. Adding another listener does not necessarily require changing the function that emits the event.
However, the listeners are still part of the same application’s execution unless you deliberately introduce processes, workers, queues, or other infrastructure. An EventEmitter by itself is not a distributed messaging system.
6. Are EventEmitter Listeners Asynchronous?
This is one of the most important details in Node.js EventEmitter.
EventEmitter calls its registered listeners synchronously, in the order they were registered. Calling .emit() does not automatically schedule those listener functions for later.
Consider this example:
import { EventEmitter } from "node:events";
const emitter = new EventEmitter();
emitter.on("process", () => {
console.log("Listener executed");
});
console.log("Before emit");
emitter.emit("process");
console.log("After emit");
Output:
Before emit
Listener executed
After emit
Why does this happen?
- Node.js executes
console.log("Before emit"). - It calls
.emit("process"). - The listener executes immediately as part of that call.
- The listener finishes, and
.emit()returns. - Node.js executes
console.log("After emit").
The event emitter does not wait for a later event-loop phase before invoking an ordinary listener.
What happens if a listener performs slow work?
Consider a listener that performs a long synchronous calculation:
emitter.on("reportReady", () => {
// Imagine a large amount of CPU-intensive synchronous work here.
console.log("Report processed");
});
That work runs on the same JavaScript execution thread. While it is running, the event loop cannot process other JavaScript callbacks on that thread.
This connects directly to the Node.js event loop and non-blocking I/O: event listeners are not automatically non-blocking merely because they are part of an event-driven system.
Keep listeners reasonably short. For expensive CPU work, consider approaches such as worker threads, depending on the task.
Can a listener start asynchronous work?
Yes. A listener can start asynchronous operations, such as a file read or a database request.
emitter.on("reportRequested", async () => {
try {
const report = await loadReport();
console.log("Report loaded:", report);
} catch (error) {
console.error("Could not load report:", error);
}
});
This is illustrative code; loadReport() represents an asynchronous function supplied by your application.
The listener begins executing when the event is emitted. When it reaches an await that has not yet completed, the async function can yield while its operation continues. Other JavaScript work can proceed, and the listener resumes when the awaited operation settles.
But there is a critical distinction: EventEmitter does not automatically wait for an async listener’s returned Promise to settle.
For example:
emitter.on("save", async () => {
await saveData();
console.log("Save finished");
});
emitter.emit("save");
console.log("Event emitted");
The output may be:
Event emitted
Save finished
The exact completion timing depends on saveData(). The key point is that .emit() does not wait for the async listener to finish.
Also, an error thrown inside an async listener can become a rejected Promise. By default, EventEmitter does not automatically handle that rejection for you. Use try...catch in the listener or another deliberate rejection-handling strategy.
Node.js supports an option called captureRejections for certain EventEmitter use cases, but it is worth learning ordinary Promise error handling before relying on it.
7. Handling Errors in Node.js EventEmitter
Node.js treats the event named "error" specially.
When an emitter emits an "error" event and no appropriate error listener is registered, Node.js throws the supplied error. An unhandled error can terminate the process.
For example, this is dangerous:
import { EventEmitter } from "node:events";
const emitter = new EventEmitter();
emitter.emit("error", new Error("Something went wrong"));
Without an error listener, this throws instead of silently ignoring the problem.
Register an error listener when your emitter uses "error" events:
import { EventEmitter } from "node:events";
const emitter = new EventEmitter();
emitter.on("error", (error) => {
console.error("Emitter error:", error.message);
});
emitter.emit("error", new Error("Connection failed"));
Output:
Emitter error: Connection failed
This listener handles the emitted error, allowing the program to continue past that particular emission, provided no other failure occurs.
There is an important design distinction here:
- An event such as
"orderCreated"announces something that happened. - An error event such as
"error"communicates a failure using EventEmitter’s special error-handling convention.
Do not assume that registering a generic listener for "error" catches every exception in your application. It handles errors emitted by that particular emitter; it is not a global exception handler.
For asynchronous listener failures, use deliberate Promise error handling as described above.
8. Building a Custom EventEmitter Class
The previous examples used an emitter directly. In real applications, it can be useful to give a particular service its own events.
For example, an order service can emit "orderCreated" after it creates an order.
Create order-service.mjs:
import { EventEmitter } from "node:events";
class OrderService extends EventEmitter {
createOrder(product, quantity) {
const order = {
id: 1001,
product,
quantity,
status: "created",
};
console.log(`Order ${order.id} created`);
this.emit("orderCreated", order);
return order;
}
}
const orderService = new OrderService();
// Listener 1: confirmation
orderService.on("orderCreated", (order) => {
console.log(`Confirmation for order ${order.id}`);
});
// Listener 2: activity log
orderService.on("orderCreated", (order) => {
console.log(`Logged order status: ${order.status}`);
});
orderService.createOrder("Keyboard", 2);
Output:
Order 1001 created
Confirmation for order 1001
Logged order status: created
Let’s unpack the design.
Why extend EventEmitter?
class OrderService extends EventEmitter {
This makes OrderService inherit EventEmitter’s methods, including .on(), .once(), .emit(), and .off().
Because OrderService does not define its own constructor, JavaScript supplies the derived-class constructor behavior that calls the parent constructor. If you add a custom constructor later, call super() before accessing this.
Why emit the event inside createOrder()?
this.emit("orderCreated", order);
The order service is in the right place to announce that an order has been created. It passes the order object as event data so listeners can use it.
Why are the listeners registered outside the class?
orderService.on("orderCreated", (order) => {
console.log(`Confirmation for order ${order.id}`);
});
The listeners define reactions without having to place every reaction inside the order-creation method.
This separation is useful as an application grows. A logging component, notification component, or other service can register its own response to the event.
For this example, the listeners run synchronously. The confirmation and activity-log messages appear before the call to createOrder() returns.
In a production application, creating an order and sending an email may have different reliability requirements. An in-memory event emitter is useful for coordinating code inside one running process, but it does not guarantee that an email will be delivered or that an event survives a process crash. For durable background work, a queue or messaging system may be more appropriate.
9. How EventEmitter Connects to the Observer Pattern
The Observer pattern describes a relationship in which one object, often called the subject, maintains a set of observers and notifies them when something changes.
Node.js EventEmitter provides a practical way to implement similar behavior.
The mapping is straightforward:
| Observer pattern concept | EventEmitter equivalent |
|---|---|
| Subject or publisher | The emitter object |
| Observer or subscriber | The listener function |
| Subscription | .on() or .once() |
| Notification | .emit() |
| Unsubscribe | .off() or another removal method |
The event emitter does not need to know the internal purpose of each listener. It only needs to know which event name was emitted and which listeners are registered for it.
This supports loose coupling: components can coordinate through an agreed event name and data shape instead of directly calling each other’s methods.
However, EventEmitter is a specific API, while the Observer pattern is a general design concept. Not every implementation of the Observer pattern uses Node.js EventEmitter.
10. Where Node.js Uses Events Already
You do not have to build every emitter yourself. Many core Node.js APIs already expose events.
HTTP servers
An HTTP server can listen for incoming requests:
import { createServer } from "node:http";
const server = createServer((request, response) => {
response.end("Hello from Node.js");
});
server.listen(3000);
The createServer() callback is a convenient request handler. Underneath, Node’s HTTP server exposes a "request" event, and its server object also provides other events associated with server operation.
TCP networking
A server built with node:net can react when a client connects. Socket objects also emit events such as "data", "end", and "error".
Streams
Readable streams can emit events when data becomes available, when the stream ends, or when an error occurs. Writable streams expose events associated with writing and completion.
Streams have additional rules around buffering, backpressure, and flow control, so they should not be treated as nothing more than generic EventEmitters. Still, the event model helps explain how they communicate state and activity.
These examples connect the event system to the Node.js APIs you have already studied: HTTP, TCP networking, and streams.
11. Common EventEmitter Mistakes
Registering a listener after the event was emitted
EventEmitter is not a history store. It does not automatically replay past events to listeners that subscribe later.
emitter.emit("ready");
emitter.on("ready", () => {
console.log("Ready!");
});
In this example, "Ready!" is never printed because the listener was registered after the event had already been emitted.
If a consumer must know the current state, store that state separately or choose a mechanism that supports state retrieval or replay.
Assuming that .emit() is asynchronous
Calling .emit() invokes listeners synchronously. If a listener performs expensive synchronous work, it can block other JavaScript execution on the thread.
Forgetting to remove listeners
Listeners can retain references to functions and objects. Long-lived emitters with repeatedly added listeners can contribute to memory leaks or unexpected repeated execution.
Node.js may warn when too many listeners are attached to the same event. The default listener threshold is intended as a warning, not a strict maximum or an automatic fix. Investigate why listeners are accumulating instead of immediately increasing the limit.
Assuming listener return values are collected
The return values of listeners are ignored by .emit(). If a caller needs a result from a function, a direct method call or a Promise-based interface may be a better fit.
Treating events as a replacement for every function call
Events are useful when a component needs to announce something that other parts of the application may care about. They are not always the best tool for a direct request-response operation.
For example, if one function needs a calculated total immediately, a normal function returning that total is usually clearer than emitting an event and attempting to retrieve the result through a listener.
12. EventEmitter vs. Direct Function Calls
Both techniques are valuable, but they solve different problems.
| Direct function call | EventEmitter |
|---|---|
| The caller names the function to execute. | The publisher emits a named event. |
| The caller receives the function’s return value, if any. | .emit() reports whether listeners existed, not their return values. |
| The caller usually knows which component it is invoking. | The publisher does not need to know every listener. |
| Often suitable for request-response work. | Often suitable for notifying multiple interested components. |
Use direct calls when one component explicitly asks another to perform work and needs a result. Use events when a component announces that something happened and separate components may need to react.
A useful rule is to choose the simplest communication mechanism that fits the problem. Event-driven code is not automatically better just because Node.js supports it.
13. A Quick Practice Exercise
Try extending the following example yourself.
import { EventEmitter } from "node:events";
const emitter = new EventEmitter();
emitter.on("downloadComplete", (fileName) => {
console.log(`${fileName} downloaded`);
});
emitter.once("downloadComplete", () => {
console.log("First download finished");
});
emitter.emit("downloadComplete", "report.pdf");
emitter.emit("downloadComplete", "image.png");
Before running it, predict the output.
Then experiment by using .off() to remove the first listener, adding a third listener, or emitting the event a third time. Pay attention to which callbacks execute and in what order.
These small changes make it easier to understand listener registration, one-time listeners, event arguments, and event order.
Conclusion
Node.js EventEmitter is a core building block for understanding Node.js event-driven architecture. It allows objects to announce events and lets registered listener functions respond without forcing the publisher to call every interested component directly.
The most important methods to remember are:
on()registers a listener for repeated events.once()registers a listener that runs at most once.emit()announces an event and passes arguments to its listeners.off()removes a particular registered listener.removeAllListeners()removes listeners broadly, so use it carefully.
Also remember the details that matter in real applications: EventEmitter listeners run synchronously, .emit() does not wait for asynchronous listeners, error events need deliberate handling, and events are not automatically stored for late subscribers.
Once these ideas are comfortable, it becomes easier to understand how Node.js servers, sockets, and streams expose events, and how to use events to organize a growing application.
Official Resources
- Node.js Events API documentation: The official reference for EventEmitter, listener methods, error events, and related APIs.
- Node.js HTTP documentation: Learn how the HTTP server and client APIs work.
- Node.js Streams documentation: Explore streams, data events, and stream lifecycle behavior.
- Node.js Net documentation: Learn about TCP servers, sockets, and networking events.