events.once
History
signal option is supported now.events.once(emitter, name, options?): Promise
EventEmitterObjectAbortSignalPromiseCreates a Promise that is fulfilled when the EventEmitter emits the given
event or that is rejected if the EventEmitter emits 'error' while waiting.
The Promise will resolve with an array of all the arguments emitted to the
given event.
This method is intentionally generic and works with the web platform
EventTarget interface, which has no special
'error' event semantics and does not listen to the 'error' event.
import { once, EventEmitter } from 'node:events'; import process from 'node:process'; const ee = new EventEmitter(); process.nextTick(() => { ee.emit('myevent', 42); }); const [value] = await once(ee, 'myevent'); console.log(value); const err = new Error('kaboom'); process.nextTick(() => { ee.emit('error', err); }); try { await once(ee, 'myevent'); } catch (err) { console.error('error happened', err); }
const { once, EventEmitter } = require('node:events'); async function run() { const ee = new EventEmitter(); process.nextTick(() => { ee.emit('myevent', 42); }); const [value] = await once(ee, 'myevent'); console.log(value); const err = new Error('kaboom'); process.nextTick(() => { ee.emit('error', err); }); try { await once(ee, 'myevent'); } catch (err) { console.error('error happened', err); } } run();
The special handling of the 'error' event is only used when events.once()
is used to wait for another event. If events.once() is used to wait for the
'error' event itself, then it is treated as any other kind of event without
special handling:
import { EventEmitter, once } from 'node:events'; const ee = new EventEmitter(); once(ee, 'error') .then(([err]) => console.log('ok', err.message)) .catch((err) => console.error('error', err.message)); ee.emit('error', new Error('boom')); // Prints: ok boom
const { EventEmitter, once } = require('node:events'); const ee = new EventEmitter(); once(ee, 'error') .then(([err]) => console.log('ok', err.message)) .catch((err) => console.error('error', err.message)); ee.emit('error', new Error('boom')); // Prints: ok boom
An AbortSignal can be used to cancel waiting for the event:
import { EventEmitter, once } from 'node:events'; const ee = new EventEmitter(); const ac = new AbortController(); async function foo(emitter, event, signal) { try { await once(emitter, event, { signal }); console.log('event emitted!'); } catch (error) { if (error.name === 'AbortError') { console.error('Waiting for the event was canceled!'); } else { console.error('There was an error', error.message); } } } foo(ee, 'foo', ac.signal); ac.abort(); // Prints: Waiting for the event was canceled!
const { EventEmitter, once } = require('node:events'); const ee = new EventEmitter(); const ac = new AbortController(); async function foo(emitter, event, signal) { try { await once(emitter, event, { signal }); console.log('event emitted!'); } catch (error) { if (error.name === 'AbortError') { console.error('Waiting for the event was canceled!'); } else { console.error('There was an error', error.message); } } } foo(ee, 'foo', ac.signal); ac.abort(); // Prints: Waiting for the event was canceled!
It is important to be aware of execution order when using the events.once()
method to await multiple events.
Conventional event listeners are called synchronously when the event is emitted. This guarantees that execution will not proceed beyond the emitted event until all listeners have finished executing.
The same is not true when awaiting Promises returned by events.once().
Promise tasks are not handled until after the current execution stack runs to
completion, which means that multiple events could be emitted before
asynchronous execution continues from the relevant await statement.
As a result, events can be "missed" if a series of await events.once()
statements is used to listen to multiple events, since there might be times
where more than one event is emitted during the same phase of the event loop.
(The same is true when using process.nextTick() to emit events, because the
tasks queued by process.nextTick() are executed before Promise tasks.)
import { EventEmitter, once } from 'node:events'; import process from 'node:process'; const myEE = new EventEmitter(); async function listen() { await once(myEE, 'foo'); console.log('foo'); // This Promise will never resolve, because the 'bar' event will // have already been emitted before the next line is executed. await once(myEE, 'bar'); console.log('bar'); } process.nextTick(() => { myEE.emit('foo'); myEE.emit('bar'); }); listen().then(() => console.log('done'));
const { EventEmitter, once } = require('node:events'); const myEE = new EventEmitter(); async function listen() { await once(myEE, 'foo'); console.log('foo'); // This Promise will never resolve, because the 'bar' event will // have already been emitted before the next line is executed. await once(myEE, 'bar'); console.log('bar'); } process.nextTick(() => { myEE.emit('foo'); myEE.emit('bar'); }); listen().then(() => console.log('done'));
To catch multiple events, create all of the Promises before awaiting any of
them. This is usually made easier by using Promise.all(), Promise.race(),
or Promise.allSettled():
import { EventEmitter, once } from 'node:events'; import process from 'node:process'; const myEE = new EventEmitter(); async function listen() { await Promise.all([ once(myEE, 'foo'), once(myEE, 'bar'), ]); console.log('foo', 'bar'); } process.nextTick(() => { myEE.emit('foo'); myEE.emit('bar'); }); listen().then(() => console.log('done'));
const { EventEmitter, once } = require('node:events'); const myEE = new EventEmitter(); async function listen() { await Promise.all([ once(myEE, 'bar'), once(myEE, 'foo'), ]); console.log('foo', 'bar'); } process.nextTick(() => { myEE.emit('foo'); myEE.emit('bar'); }); listen().then(() => console.log('done'));