events.once()
Await the next emission of an event as a promise that rejects if 'error' fires first.
events.once(emitter, name[, options])- Added in Node.js v11.13.0, v10.16.0
Parameters
| Parameter | Type | Description |
|---|---|---|
$emitter | <EventEmitter> | The emitter to wait on. An EventTarget is also accepted. |
$name | <string> | <symbol> | The name of the event to wait for. |
$optionsoptional | <Object> | Options object. |
$options.signaloptional | <AbortSignal> | Can be used to cancel waiting for the event; aborting rejects the promise with an AbortError. |
Return value
<Promise>A promise fulfilled with an array of the arguments passed to the next emit of name, or rejected with the error if the emitter emits 'error' first.
Errors
Rejects with the emitted error if the emitter emits 'error' while waiting, and with an AbortError (code ERR_ABORT_ERR) if the supplied AbortSignal is aborted. Waiting for 'error' itself makes the event resolve normally rather than reject.
Changelog
- PHP v15.0.0The signal option is supported now, so a pending wait can be aborted instead of hanging forever.
events.once() is the module-level helper — not the instance method — that turns the next emission of an event into an awaitable promise. It resolves with an array of every argument passed to emit(), so destructure: const [host, port] = await once(conn, 'ready').
When to reach for it
Use it any time async/await code needs to pause until an emitter signals something once: a server's 'listening', a socket's 'connect', a child process's 'exit', a stream's 'end'. It replaces the hand-rolled new Promise wrapper that races 'ready' against 'error' — and it removes the losing listener for you, which is the part people forget.
What trips people up
It rejects on 'error' automatically, which is usually what you want, except when the event you are waiting for is 'error' — then it resolves with the error object instead.
Awaiting before the emit is scheduled is a real hazard: if the event already fired, or fires synchronously before your await, the promise never settles. Pass an AbortSignal — AbortSignal.timeout(5000) is enough — so a missed event fails loudly rather than hanging the process.
Example
When to use it
- Await server 'listening' in an async bootstrap before running migrations
- Wait for a socket 'connect' with a five second AbortSignal.timeout guard
- Pause until a stream emits 'end' inside an async function
Frequently asked questions
How do I await an event in Node.js?
Use the module-level events.once(emitter, name): `const [value] = await once(emitter, 'ready')`. It resolves with an array of the arguments passed to emit and rejects if the emitter emits 'error' first. Add { signal } so it cannot hang forever.
What is the difference between events.once() and emitter.once()?
emitter.once() registers a callback and returns the emitter; events.once() is a static helper that returns a promise you await. Use the method when you want a handler, the function when you want to suspend an async function until the event happens.
Why does my await once() never resolve?
Because the event fired before the await was reached, or nothing ever emits it. once() only catches future emissions, so a synchronous emit in the same tick is missed. Schedule the emit with setImmediate(), or pass { signal: AbortSignal.timeout(ms) } so the wait rejects instead of hanging.
More examples
Awaiting an HTTP server's 'listening' event
listen() is asynchronous, so server.address() is null until 'listening' fires. once() turns both ends of the lifecycle into ordinary awaits, and because it also rejects on 'error', a port-in-use failure surfaces as a thrown exception at the await rather than as an uncaught 'error' event.
import http from 'node:http';
import { once } from 'node:events';
const server = http.createServer((req, res) => res.end('ok'));
server.listen(0);
await once(server, 'listening');
console.log('bound to port', server.address().port);
server.close();
await once(server, 'close');
console.log('server closed');Guarding against an event that never arrives
AbortSignal.timeout() rejects the promise after 500 ms with a TimeoutError whose code is ABORT_ERR, and once() removes both the 'ready' and 'error' listeners it had installed — the count prints 0. Treat any await on an emitter you do not control as needing a signal; a hung await produces no stack trace and no log line.
import { EventEmitter, once } from 'node:events';
const conn = new EventEmitter();
// Nothing ever emits 'ready' - without a signal this await hangs forever.
try {
const [host] = await once(conn, 'ready', {
signal: AbortSignal.timeout(500),
});
console.log('ready:', host);
} catch (err) {
console.log('gave up:', err.name, err.code);
console.log('listeners cleaned up:', conn.listenerCount('ready'));
}Waiting for a child process to exit
The 'exit' event carries two arguments, so the resolved array destructures to [code, signal] — a clear case where forgetting that once() resolves with an array gives you the whole array where you expected a number. Output is "hi", 3, and null.
import { spawn } from 'node:child_process';
import { once } from 'node:events';
const child = spawn('node', ['-e', 'console.log("hi"); process.exit(3)']);
let out = '';
child.stdout.on('data', (chunk) => { out += chunk; });
const [code, signal] = await once(child, 'exit');
console.log('stdout:', out.trim());
console.log('exit code:', code);
console.log('killed by signal:', signal);
Discussion