Events and EventEmitter

Node.js · Practical · 11 min

Half of Node's core API is built on EventEmitter, so you need it to understand servers, streams and processes.

An EventEmitter is an object that lets other code subscribe to named events. Node's servers, sockets, streams, child processes and process itself all extend it.

The core API:

  • emitter.on(name, listener) — call listener every time name is emitted; once only for the next one.
  • emitter.emit(name, ...args) — call every listener for name, in registration order, passing the args.
  • emitter.off(name, listener) — remove a listener (you need the same function reference).

Two facts matter more than the API. First, `emit` is synchronous: all listeners run before emit returns, on the same call stack. A slow listener slows the emitter; a throwing listener throws out of emit. If you need async behaviour, the listener must schedule it.

Second, the event named `"error"` is special. If an emitter emits "error" and nobody listens, Node throws the error and — unless caught — crashes the process. Always attach an error listener to sockets, streams and anything else that can fail.

Bridging to async/await:

  • await once(emitter, "ready") (from node:events) waits for a single event and rejects if "error" fires first.
  • for await (const [msg] of on(emitter, "message")) turns events into an async iterator.

To write your own, extend EventEmitter in a class and call this.emit(...). Node prints a MaxListenersExceededWarning when more than 10 listeners attach to one event — usually a leak where you add a listener per request and never remove it. Raise the limit with setMaxListeners only if it is genuinely intended.

Browsers' EventTarget also exists in Node; use it when you want web-compatible code, but most Node APIs speak EventEmitter.

Example

import { EventEmitter, once } from "node:events";

class Job extends EventEmitter {
  async run(steps) {
    for (let i = 1; i <= steps; i++) {
      await new Promise((r) => setTimeout(r, 50));
      this.emit("progress", i, steps);
    }
    if (steps > 3) this.emit("error", new Error("too many steps"));
    else this.emit("done", { steps });
  }
}

const job = new Job();
job.on("progress", (i, n) => console.log(`step ${i}/${n}`));
job.on("error", (err) => console.error("job failed:", err.message)); // without this: crash

job.run(3);
const [result] = await once(job, "done");   // promise-style wait
console.log("finished", result);            // finished { steps: 3 }

Remember

  • emit calls listeners synchronously, in order
  • An unhandled 'error' event throws and usually crashes the process
  • events.once / events.on bridge events to async/await

Watch out for

  • Adding listeners inside a request handler and never removing them (memory leak + MaxListeners warning).

Check yourself

  1. An emitter emits 'error' with no 'error' listener attached. What happens?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: nodejs.org/docs/latest-v22.x/api/events.html