events.on()

Turn a repeating emitter event into an async iterator you can consume with for await.

Signature
events.on(emitter, eventName[, options])
  • Added in Node.js v13.6.0, v12.16.0

Parameters

ParameterTypeDescription
$emitter<EventEmitter>The emitter to iterate. An EventTarget is also accepted.
$eventName<string> | <symbol>The name of the event being listened for.
$optionsoptional<Object>Options object.
$options.signaloptional<AbortSignal>Can be used to cancel awaiting events; aborting ends the loop by throwing an AbortError.
$options.closeoptional<string>[]Names of events that will end the iteration normally, without throwing.
$options.highWaterMarkoptional<integer>= Number.MAX_SAFE_INTEGERThe emitter is paused every time the size of the buffered events is higher than this. Supported only on emitters implementing pause() and resume().
$options.lowWaterMarkoptional<integer>= 1The emitter is resumed every time the size of the buffered events is lower than this. Supported only on emitters implementing pause() and resume().

Return value

<AsyncIterator>An AsyncIterator that yields, for each emission, an array of the arguments passed to emit().

Errors

Throws if the emitter emits 'error' while iterating, and throws an AbortError when the supplied AbortSignal is aborted. All listeners it added are removed when the loop exits by any route.

Changelog

  • PHP v22.0.0, v20.13.0Renamed to highWaterMark and lowWaterMark for consistency; the old highWatermark and lowWatermark spellings are still supported.
  • PHP v20.0.0The close, highWatermark and lowWatermark options were added. Before v20 there was no way to end the loop cleanly other than aborting a signal.

events.on() wraps an emitter in an async iterator so a repeating event can be consumed with for await. Each iteration yields an array of the arguments given to emit(), so destructure in the loop head: for await (const [job] of on(bus, 'job')).

When to reach for it

Reach for it when the consumer is naturally sequential and wants await inside the handler — processing queued jobs one at a time, draining a message feed, reading lines. A callback registered with on() cannot make the producer wait; the iterator can, because events emitted while you are awaiting are buffered until you ask for the next one.

What trips people up

The loop never ends on its own. There is no natural completion — you must pass an AbortSignal, or list terminating events in close: ['end'] (Node 20+). Without one of those, the for await keeps the process alive indefinitely.

The buffer is unbounded by default. If the producer outpaces the consumer, events pile up in memory; highWaterMark only helps on emitters that implement pause() and resume(). On the way out — abort, break, or a throw — every listener it installed is removed for you.

Example

Try it yourself
Loading editor…
Press Run to execute the code.

When to use it

  • Process queued jobs one at a time with await inside the loop body
  • Consume a message feed until an AbortController cancels the subscriber
  • Read lines from a readline interface and stop on its 'close' event

Frequently asked questions

How do I use for await with an EventEmitter in Node.js?

Wrap it with the module-level events.on(): `for await (const [arg] of on(emitter, 'event', { signal }))`. Each iteration yields an array of the emit arguments. Always supply an AbortSignal or a close option, because the loop has no natural termination.

How do I stop a for await loop over events.on()?

Three ways: abort an AbortSignal passed in options (the loop throws an AbortError), list terminating events in { close: ['end'] } on Node 20+ (the loop returns normally), or break out of the loop yourself. All three remove every listener events.on() installed.

What is the difference between events.on() and events.once()?

events.once() returns a promise for the next single emission; events.on() returns an async iterator over every emission. Use once() to await one lifecycle event like 'ready', and on() to consume a repeating stream of events such as 'message' or 'line'.

More examples

Ending the loop cleanly with the close option

With close: ['end'] the for-await exits normally when 'end' fires, so no try/catch is needed — compare with the AbortSignal route, which always throws an AbortError. The close option requires Node 20.0.0 or later; on earlier versions a signal is the only way to stop the loop.

Example · javascript
import { EventEmitter, on } from 'node:events';

const reader = new EventEmitter();
setTimeout(() => {
  reader.emit('line', 'first');
  reader.emit('line', 'second');
  reader.emit('end');
}, 10);

// close: [...] ends the iteration by returning, not by throwing.
for await (const [line] of on(reader, 'line', { close: ['end'] })) {
  console.log('line:', line);
}

console.log('iteration finished cleanly');

Backpressure: awaiting inside the loop body

Tasks are processed strictly one at a time even though the producer emits ten times faster, because the iterator buffers everything emitted while the body awaits. That buffer is unbounded, which is the trade-off: on a long-lived fast producer it is a memory leak, and highWaterMark only throttles emitters that expose pause() and resume().

Example · javascript
import { EventEmitter, on } from 'node:events';

const queue = new EventEmitter();
const ac = new AbortController();

// Producer runs far ahead of the consumer.
let n = 0;
const timer = setInterval(() => queue.emit('task', ++n), 1);
setTimeout(() => { clearInterval(timer); ac.abort(); }, 60);

try {
  for await (const [task] of on(queue, 'task', { signal: ac.signal })) {
    // Events emitted during this await are buffered, not dropped.
    await new Promise((r) => setTimeout(r, 10));
    console.log('finished task', task);
  }
} catch (err) {
  if (err.name !== 'AbortError') throw err;
}
console.log('consumer stopped');

Iterating a readline interface

readline emits 'line' repeatedly and 'close' once at EOF, which maps exactly onto on() with close: ['close']. Output is the three lines upper-cased and "total lines: 3". In practice readline already implements Symbol.asyncIterator, so `for await (const line of rl)` is shorter here — events.on() is what you use for emitters that do not.

Example · javascript
import fs from 'node:fs';
import readline from 'node:readline';
import { on } from 'node:events';

fs.writeFileSync('lines.txt', 'alpha\nbeta\ngamma\n');

const rl = readline.createInterface({
  input: fs.createReadStream('lines.txt'),
});

let count = 0;
for await (const [line] of on(rl, 'line', { close: ['close'] })) {
  count++;
  console.log(count, line.toUpperCase());
}

console.log('total lines:', count);
fs.unlinkSync('lines.txt');

Discussion

  • Be the first to comment on this lesson.