events.once() to await a single event without a listener callback
Published January 8, 2027
import { EventEmitter, once } from "node:events";
export const waitForReady = (emitter: EventEmitter) => once(emitter, "ready");
const bus = new EventEmitter();
setTimeout(() => bus.emit("ready", { startedAt: Date.now() }), 100);
const [payload] = await waitForReady(bus);
console.log(payload);What
once(emitter, eventName) from node:events returns a promise that resolves with the event's arguments the first time that event fires — no listener callback to write, and no manual cleanup afterward.
Why it matters
Waiting for exactly one occurrence of an event with the plain EventEmitter API means writing the callback-style dance every time: attach a listener with .on(), do whatever you actually wanted to do inside the callback, and remember to call .off() yourself so the listener doesn't outlive its usefulness (or use .once(), which handles removal but still leaves you writing a callback and manually bridging back to a promise if the rest of your code is async/await).
events.once() collapses that into an expression you can await directly, right in line with the rest of your async code — no separate callback, no manual listener teardown, no promise-wrapping boilerplate. It also correctly rejects if the emitter emits 'error' while you're waiting, so an error doesn't just vanish because nothing was listening for it directly.
How it works
once(emitter, "ready")returns a promise; awaiting it suspends untilbus.emit("ready", ...)runs.- The resolved value is always an array of every argument passed to
emit—[payload]here destructures the single argument this event happens to carry. waitForReadyis justoncepartially applied to a specific event name — a thin wrapper so callers don't need to know the emitter also fires other events they're not interested in.- Nothing about this is polling: the promise is driven by the event itself firing, not by checking some state on a timer.
Gotchas
- The promise resolves with all arguments as an array, even if the event only ever emits one value — forgetting the destructuring brackets is an easy mistake (
const payload = await once(...)gives you[payload], notpayload). - If the event you're waiting for never fires, the
awaithangs forever. Pass{ signal }with anAbortController(orAbortSignal.timeout()) if there's any chance of that. once()only catches the first emission after you start waiting — if the event already fired before you called it, that occurrence is gone.
Related
AbortSignal.timeout()— pairs naturally withonce()'ssignaloption to cap how long you're willing to wait.stream.finished()— a more specific version of the same idea, purpose-built for knowing when a stream has fully ended (or errored) rather than any arbitrary event.