Events & EventEmitter
A web server waits for requests. A file stream waits for the next chunk of data. A chat app waits for messages. In each case, something happens at an unknown time and your code must react. Node.js handles this with events: an object announces "this just happened", and any code that registered interest runs.
The class behind this is EventEmitter from the node:events module. HTTP servers, streams, sockets and even process are event emitters, so once you understand it, a large part of Node.js makes sense. In this lesson you will create emitters, listen to them, handle error events correctly and wait for events with await.
Listening and emitting
An emitter has two main methods. on(name, listener) registers a function, called a listener, for an event name. emit(name, ...args) fires the event and passes the arguments to every listener.
import { EventEmitter } from "node:events";
const shop = new EventEmitter();
shop.on("order", (item, qty) => {
console.log(`Kitchen: prepare ${qty} x ${item}`);
});
shop.on("order", (item, qty) => {
console.log(`Billing: add ${qty} x ${item} to the bill`);
});
shop.once("open", () => console.log("Shop opened (runs only once)"));
shop.emit("open");
shop.emit("open");
shop.emit("order", "Masala dosa", 2);
console.log(`Listeners for "order": ${shop.listenerCount("order")}`);
console.log(shop.emit("refund")); // no listenersShop opened (runs only once) Kitchen: prepare 2 x Masala dosa Billing: add 2 x Masala dosa to the bill Listeners for "order": 2 false
Points to notice:
- An event can have many listeners. They run in the order they were added.
once()registers a listener that removes itself after the first call, so the second"open"did nothing.- Event names are just strings you choose.
emit()returnstrueif the event had listeners andfalseif nobody was listening.
The kitchen and billing code do not know about each other. The shop only announces an order. This loose connection is the main benefit of events: you can add a new listener, such as an SMS sender, without touching the code that emits the event.
Listeners run synchronously
emit() calls every listener immediately, one after another, before it returns. It does not wait for the event loop:
import { EventEmitter } from "node:events";
const bell = new EventEmitter();
bell.on("ring", () => console.log("Listener runs"));
console.log("Before emit");
bell.emit("ring");
console.log("After emit");Before emit Listener runs After emit
So a slow listener slows down the code that called emit(). If a listener needs to do async work, such as saving to a database, make it an async function, but remember that emit() will not wait for its promise.
Removing listeners
Remove a listener with off() (an alias of the older name removeListener()). You must pass the same function, so store it in a variable instead of writing an inline arrow function:
import { EventEmitter } from "node:events";
const chat = new EventEmitter();
function showMessage(text) {
console.log(`New message: ${text}`);
}
chat.on("message", showMessage);
chat.emit("message", "Hi Meera!");
chat.off("message", showMessage);
chat.emit("message", "Are you there?"); // nobody is listening now
console.log(chat.listenerCount("message"));New message: Hi Meera! 0
If you add more than 10 listeners for one event on one emitter, Node.js prints a MaxListenersExceededWarning: Possible EventEmitter memory leak detected. It usually means you add a listener inside a loop or a request handler and never remove it. Fix the bug rather than raising the limit with setMaxListeners().
Your own emitter class
In real code you usually extend EventEmitter, so your class can announce what happens inside it:
import { EventEmitter } from "node:events";
class TrainTracker extends EventEmitter {
#delayMinutes = 0;
addDelay(minutes) {
this.#delayMinutes += minutes;
this.emit("delay", this.#delayMinutes);
if (this.#delayMinutes >= 60) {
this.emit("late", `Running ${this.#delayMinutes} minutes late`);
}
}
}
const rajdhani = new TrainTracker();
rajdhani.on("delay", (total) => console.log(`SMS: delay is now ${total} min`));
rajdhani.on("late", (message) => console.log(`Announcement: ${message}`));
rajdhani.addDelay(25);
rajdhani.addDelay(40);SMS: delay is now 25 min SMS: delay is now 65 min Announcement: Running 65 minutes late
Node.js's own classes work exactly like this. An http.Server emits "request" and "listening", a readable stream emits "data" and "end", and process emits "exit" and signals such as "SIGINT".
The special "error" event
The event name "error" has a special rule. If you emit "error" and no listener is registered for it, Node.js throws the error, and an uncaught error stops your program. With a listener, the error is handled normally:
import { EventEmitter } from "node:events";
const payment = new EventEmitter();
payment.on("error", (err) => {
console.log(`Payment failed: ${err.message}`);
});
payment.emit("error", new Error("UPI server timeout"));
console.log("Program continues");Payment failed: UPI server timeout Program continues
Remove the payment.on("error", ...) line and the program crashes with Error: UPI server timeout. This is why you should always add an "error" listener to servers, streams and sockets: a network problem should be logged, not crash the whole app.
Advanced: waiting for events with await
The node:events module also has two helper functions that turn events into promises. once(emitter, name) returns a promise for the next event's arguments, and on(emitter, name) gives an async iterator you can loop over with for await:
import { EventEmitter, once, on } from "node:events";
const exam = new EventEmitter();
setTimeout(() => exam.emit("result", "Kabir", 91), 100);
const [student, marks] = await once(exam, "result");
console.log(`${student} scored ${marks}`);
const ticker = new EventEmitter();
setTimeout(() => {
ticker.emit("score", "IND 45/1");
ticker.emit("score", "IND 52/1");
ticker.emit("score", "IND 60/2");
}, 50);
let count = 0;
for await (const [score] of on(ticker, "score")) {
console.log(`Live: ${score}`);
if (++count === 3) break;
}
console.log("Stopped listening");Kabir scored 91 Live: IND 45/1 Live: IND 52/1 Live: IND 60/2 Stopped listening
The arguments always come as an array, so destructure them. once() also rejects if the emitter emits "error" first, so a try...catch around the await handles failures. break stops the loop and removes the listener.
Common mistakes
- No
"error"listener. An emitted error without a listener crashes the process. - Removing an inline function.
off("x", () => {})creates a new function, so nothing is removed. Keep a reference. - Adding listeners inside a request handler. Each request adds one more and they pile up. Add listeners once, at startup.
- Expecting
emit()to wait for async listeners. It does not; listeners are called and their promises are ignored. - Misspelling event names.
emit("ordr")silently reaches no one. Store names in constants if you use them in many places.
Key points
EventEmitterfromnode:eventslets objects announce events;on()listens andemit()fires.- Listeners run synchronously, in the order they were added;
once()listens a single time. - Remove a listener with
off()and the same function reference. - Extend
EventEmitterto make your own classes emit events, as servers and streams do. - Always listen for
"error": an unhandled error event crashes the program. events.once()andevents.on()let youawaitevents and loop over them withfor await.
Exercise
Create a class Classroom that extends EventEmitter. It has a method enter(name) that keeps a count of students and emits "joined" with the name and the new count. When the count reaches 3, it also emits "full". Add listeners that print Isha joined (1/3) style messages and Classroom is full, then call enter() for three students.
Show solution
import { EventEmitter } from "node:events";
class Classroom extends EventEmitter {
#count = 0;
enter(name) {
this.#count++;
this.emit("joined", name, this.#count);
if (this.#count === 3) this.emit("full");
}
}
const room = new Classroom();
room.on("joined", (name, count) => console.log(`${name} joined (${count}/3)`));
room.once("full", () => console.log("Classroom is full"));
room.enter("Isha");
room.enter("Rohan");
room.enter("Ananya");Isha joined (1/3) Rohan joined (2/3) Ananya joined (3/3) Classroom is full
The class only announces what happens; the printing lives in the listeners, so you could add a second "full" listener that sends an email without changing the class.
Quick Quiz
1. What does emitter.once("ready", fn) do?
once registers a one-time listener; later events do not call it.
2. When does a listener added with on() run, relative to emit()?
emit() calls its listeners synchronously, in the order they were added.
3. What happens when an emitter emits "error" and has no "error" listener?
The "error" event is special: without a listener the error is thrown.
4. Why does chat.off("message", (t) => console.log(t)) not remove the listener you added earlier?
Listeners are removed by reference, so keep the original function in a variable.
Learn Node.js with a mentor
Free lessons get you started. Our Full Stack Web Development training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
