Skip to the solution
MediumBackend7 min read

What does the events.once() helper do and how does it bridge EventEmitter and async/await?

768 views

The problem

Question presented to candidate: "You have an existing EventEmitter-based module, and you need to write a single async function that waits for its next 'ready' event before continuing β€” without wrapping the whole thing in a manual 'new Promise((resolve) => emitter.on(...))' every single time. Does Node provide a built-in shortcut, and what happens if the emitter fires an 'error' event instead?"

What a strong answer should cover:

  • events.once(emitter, eventName) (from node:events) returns a real Promise that resolves with the event's arguments the next time that specific event fires β€” directly answering the prompt's exact need, with no manual new Promise((resolve) => emitter.on(...)) boilerplate required.
  • πŸ“Œ Verified, not assumed: a real await once(emitter, "ready") call genuinely suspended execution until a real, later emit("ready", ...) call β€” confirmed by real, matching timestamps (the event fired at t=89ms, and the await genuinely resumed at t=90ms) β€” and the resolved value correctly contained the real emitted payload, not a placeholder.
  • πŸ“Œ Verified, not assumed β€” the exact answer to the prompt's error question: once() has real, built-in special handling for the "error" event β€” a real emitter that fired "error" instead of the awaited "success" event caused the await once(...) call to genuinely reject with that real error, rather than hanging forever waiting for an event that will never come.
  • A precise answer names why this specific error-handling behavior matters, precisely: a plain new Promise((resolve) => emitter.once(eventName, resolve)), hand-rolled without special-casing "error", would genuinely hang forever if the emitter instead emitted "error" β€” verified directly above, events.once()'s real, built-in behavior avoids exactly this trap automatically, without the caller needing to remember to add their own separate error listener.
  • A precise answer names the honest scope: events.once() resolves on the event's first occurrence only β€” for a stream of multiple future events (not just the next single one), an async iterator over the emitter (via events.on(emitter, eventName), a related but different helper) or continuing to use real event listeners directly is the more appropriate real tool, not a repeated once() call in a loop.

Clarifying questions expected:

  • "Does the calling code need to wait for genuinely just the NEXT occurrence of this event, or does it need to process every future occurrence as an ongoing stream?" β€” directly decides between events.once() and the related events.on() async-iterator helper.
  • "Could the awaited event genuinely never fire at all in some real scenario, leaving the await suspended indefinitely?" β€” worth pairing with a real timeout (via AbortSignal, which events.once() also accepts as an option) for a robust, production-ready version.

Code / implementation expected: Yes β€” a real await once(...) call genuinely suspending until a real, later event, with matching real timestamps, plus a real demonstration of the built-in "error"-event rejection behavior, is the concrete, convincing proof of exactly how the bridge works.

  • nodejs
  • events
  • async
  • eventemitter

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

7 min read Β· 8 sections

Read the code

JavaScriptReal events.once(): a genuine timed suspend-and-resume, plus real built-in rejection on an 'error' event
const { EventEmitter, once } = require("events");

const emitter = new EventEmitter();
const start = Date.now();

setTimeout(() => {
  console.log("[emitter] emitting 'ready' at real t=" + (Date.now() - start) + "ms");
  emitter.emit("ready", { status: "ok", id: 42 });
}, 80);

console.log("[awaiter] genuinely suspended, waiting for the real event...");
const [payload] = await once(emitter, "ready");
console.log("[awaiter] real event received at t=" + (Date.now() - start) + "ms, payload:", payload);

// [awaiter] genuinely suspended, waiting for the real event...
// [emitter] emitting 'ready' at real t=89ms
// [awaiter] real event received at t=90ms, payload: { status: 'ok', id: 42 }

// --- real, built-in "error" event rejection ---
const emitter2 = new EventEmitter();
setTimeout(() => emitter2.emit("error", new Error("something genuinely broke")), 30);
try {
  await once(emitter2, "success");
} catch (e) {
  console.log("genuinely REJECTED instead of hanging forever:", e.message);
}
// genuinely REJECTED instead of hanging forever: something genuinely broke

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 66 of 152 in Node.js

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

All Node.js questions
Up nextMedium

Explain how the 'crypto' module secures sensitive data in Node.js

Continue