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)— calllistenerevery timenameis emitted;onceonly for the next one.emitter.emit(name, ...args)— call every listener forname, 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")(fromnode: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
emitcalls listeners synchronously, in order- An unhandled
'error'event throws and usually crashes the process events.once/events.onbridge events to async/await
Watch out for
- Adding listeners inside a request handler and never removing them (memory leak + MaxListeners warning).