An event emitter is an object that passes news from one part of a program to any other parts that want it. Code that wants to hear about something registers a function, called a listener, under an event name such as "message". Code that has news emits the event by that name, with any data, and the emitter calls every listener registered under it, passing the data along. Neither side needs to know about the other: the code that emits doesn't know who's listening, or whether anyone is, and listeners can be added and removed without touching it.
Think of a radio station. It broadcasts on its frequency whether anyone has tuned in or not, and every radio tuned to it hears the same thing. Tuning in is on, broadcasting is emit, and switching off is off.
Node has one built in, EventEmitter from the node:events module, and its four main methods are the ones most emitters share:
| Method | What it does |
|---|---|
on(name, listener) | Adds listener for the event name. An event can have any number of listeners. |
once(name, listener) | Adds a listener that runs the next time name is emitted, and is then removed. |
off(name, listener) | Removes a listener. You pass the same function you added, so keep a reference to it: an arrow function written inline can't be removed later. Some emitters also have on return a function that removes the listener, so you don't need to keep it. |
emit(name, ...args) | Calls each of the event's listeners with args, one after another in the order they were added, before emit returns. In Node, a listener written as a regular function gets the emitter as this, and emit returns true if the event had listeners and false if it had none. |
const { EventEmitter } = require("node:events");
const chat = new EventEmitter();
const greet = (name) => console.log("Hello, " + name);
chat.on("join", greet);
chat.once("join", (name) => console.log(name + " was first"));
chat.emit("join", "Ada"); // logs "Hello, Ada", then "Ada was first"; returns true
chat.emit("join", "Alan"); // logs "Hello, Alan" only: the once listener is gone
chat.off("join", greet);
chat.emit("join", "Grace"); // logs nothing and returns false: no listeners are left
In the browser, the same idea is EventTarget, which every element, document, and window inherit from. addEventListener(type, listener) is its on, removeEventListener(type, listener) its off, and addEventListener(type, listener, { once: true }) its once. The browser emits events such as click and input itself, and your code can emit its own with dispatchEvent. Node's streams and servers are EventEmitters too, which is why you write stream.on("data", ...).
A publish/subscribe hub, pub/sub for short, is the same idea under other names. Event names are called topics, often strings such as "user.created"; adding a listener is subscribing, and emitting is publishing. Some hubs also let one subscription match a whole group of topics with a wildcard pattern. An event bus is one emitter or hub that many parts of an app share, so they can talk to each other without importing each other.
Write once(emitter, event, options). emitter is an object with on(name, listener) and off(name, listener) methods, like Node's EventEmitter. When it emits an event with emitter.emit(name, ...args), it calls every listener for name with args.
once returns a promise that fulfills the next time emitter emits event, with an array of the arguments that event came with. Other events don't affect it.
If emitter emits "error" first, the promise rejects with that event's first argument, the error. When event is "error" itself, it's the event you're waiting for, so the promise fulfills with [error].
options is optional, and so is its timeout, in milliseconds. If timeout ms pass before the event, the promise rejects with an Error whose name is "TimeoutError" (the message is up to you). Without a timeout, it waits for as long as it takes.
However the promise settles, remove every listener once added and clear its timer, so nothing is left behind.
const server = new EventEmitter(); // Node's, or any object with on, off, and emit
const ready = once(server, "listening", { timeout: 1000 });
server.emit("listening", 8080, "localhost");
await ready; // [8080, "localhost"]
const started = once(server, "listening");
server.emit("error", new Error("port in use"));
await started; // rejects with Error: port in use
const slow = once(server, "listening", { timeout: 1000 });
await slow; // after 1000 ms, rejects with an Error named "TimeoutError"
let listeners = [];
const emitter = {
on: (name, fn) => listeners.push({ name, fn }),
off: (name, fn) => (listeners = listeners.filter((l) => l.name !== name || l.fn !== fn)),
emit: (name, ...args) => listeners.filter((l) => l.name === name).forEach((l) => l.fn(...args)),
};
const promise = once(emitter, "data");
emitter.emit("data", "chunk", 2);
await expect(promise).resolves.toEqual(["chunk", 2]);
let listeners = [];
const emitter = {
on: (name, fn) => listeners.push({ name, fn }),
off: (name, fn) => (listeners = listeners.filter((l) => l.name !== name || l.fn !== fn)),
emit: (name, ...args) => listeners.filter((l) => l.name === name).forEach((l) => l.fn(...args)),
};
let result;
once(emitter, "data").then((value) => {
result = value;
});
emitter.emit("progress", 50);
emitter.emit("end");
await flush();
expect(result).toBeUndefined();
emitter.emit("data", "chunk");
await flush();
expect(result).toEqual(["chunk"]);
let listeners = [];
const emitter = {
on: (name, fn) => listeners.push({ name, fn }),
off: (name, fn) => (listeners = listeners.filter((l) => l.name !== name || l.fn !== fn)),
emit: (name, ...args) => listeners.filter((l) => l.name === name).forEach((l) => l.fn(...args)),
};
const error = new Error("connection reset");
let outcome = "still pending";
once(emitter, "data").then(
(value) => (outcome = value),
(reason) => (outcome = reason),
);
emitter.emit("error", error);
await flush();
expect(outcome).toBe(error);
let listeners = [];
const emitter = {
on: (name, fn) => listeners.push({ name, fn }),
off: (name, fn) => (listeners = listeners.filter((l) => l.name !== name || l.fn !== fn)),
emit: (name, ...args) => listeners.filter((l) => l.name === name).forEach((l) => l.fn(...args)),
};
const error = new Error("connection reset");
const promise = once(emitter, "error");
emitter.emit("error", error);
await expect(promise).resolves.toHaveLength(1);
expect((await promise)[0]).toBe(error);
const clock = useFakeTimers();
let listeners = [];
const emitter = {
on: (name, fn) => listeners.push({ name, fn }),
off: (name, fn) => (listeners = listeners.filter((l) => l.name !== name || l.fn !== fn)),
emit: (name, ...args) => listeners.filter((l) => l.name === name).forEach((l) => l.fn(...args)),
};
let reason;
once(emitter, "data", { timeout: 100 }).catch((e) => {
reason = e;
});
await clock.tickAsync(99);
expect(reason).toBeUndefined();
await clock.tickAsync(1);
expect(reason).toBeInstanceOf(Error);
expect(reason.name).toBe("TimeoutError");
const clock = useFakeTimers();
let listeners = [];
const emitter = {
on: (name, fn) => listeners.push({ name, fn }),
off: (name, fn) => (listeners = listeners.filter((l) => l.name !== name || l.fn !== fn)),
emit: (name, ...args) => listeners.filter((l) => l.name === name).forEach((l) => l.fn(...args)),
};
let settled = false;
const promise = once(emitter, "data");
promise.then(
() => (settled = true),
() => (settled = true),
);
await clock.tickAsync(60000);
expect(settled).toBe(false);
emitter.emit("data", 1);
await expect(promise).resolves.toEqual([1]);
const clock = useFakeTimers();
let listeners = [];
const emitter = {
on: (name, fn) => listeners.push({ name, fn }),
off: (name, fn) => (listeners = listeners.filter((l) => l.name !== name || l.fn !== fn)),
emit: (name, ...args) => listeners.filter((l) => l.name === name).forEach((l) => l.fn(...args)),
};
const promise = once(emitter, "data", { timeout: 1000 });
emitter.emit("data");
await promise;
expect(listeners).toHaveLength(0);
expect(clock.pending()).toBe(0);
const clock = useFakeTimers();
let listeners = [];
const emitter = {
on: (name, fn) => listeners.push({ name, fn }),
off: (name, fn) => (listeners = listeners.filter((l) => l.name !== name || l.fn !== fn)),
emit: (name, ...args) => listeners.filter((l) => l.name === name).forEach((l) => l.fn(...args)),
};
const promise = once(emitter, "data", { timeout: 1000 });
promise.catch(() => {}); // it rejects; this test only checks the cleanup
emitter.emit("error", new Error("closed"));
await flush();
expect(listeners).toHaveLength(0);
expect(clock.pending()).toBe(0);
const clock = useFakeTimers();
let listeners = [];
const emitter = {
on: (name, fn) => listeners.push({ name, fn }),
off: (name, fn) => (listeners = listeners.filter((l) => l.name !== name || l.fn !== fn)),
emit: (name, ...args) => listeners.filter((l) => l.name === name).forEach((l) => l.fn(...args)),
};
const promise = once(emitter, "data", { timeout: 100 });
promise.catch(() => {}); // it rejects; this test only checks the cleanup
await clock.tickAsync(100);
expect(listeners).toHaveLength(0);
Run tests runs every test on your code, here in your browser. The problem is solved when they all pass.
Ctrl + Enter runs the tests (⌘ + Enter on a Mac).
I'm here to help with Wait for an event, with a timeout. I give hints, never the solution, so you get to solve it yourself. Ask for a hint, select code to ask about it, or press Ask Tuto why on a failing test.
Your first 3 answers need no account. for free to keep asking.