Events & EventEmitter

Node.js · Lesson 10 of 30 7 min read Updated

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.

JavaScript
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 listeners
Output
Shop 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() returns true if the event had listeners and false if 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:

JavaScript
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");
Output
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:

JavaScript
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"));
Output
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:

JavaScript
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);
Output
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:

JavaScript
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");
Output
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:

JavaScript
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");
Output
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

  • EventEmitter from node:events lets objects announce events; on() listens and emit() 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 EventEmitter to make your own classes emit events, as servers and streams do.
  • Always listen for "error": an unhandled error event crashes the program.
  • events.once() and events.on() let you await events and loop over them with for 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
JavaScript
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");
Output
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?

2. When does a listener added with on() run, relative to emit()?

3. What happens when an emitter emits "error" and has no "error" listener?

4. Why does chat.off("message", (t) => console.log(t)) not remove the listener you added earlier?

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