Some sequences arrive over time. Pages from an API, lines from a file, messages from a socket. You know there is more coming, but you have to wait for it.
for await...of handles that shape. It works with Symbol.asyncIterator, which is the same idea as Symbol.iterator except that next() returns a promise.
An async generator
async function* gives you an async iterable with no manual protocol work. Each yield can come after an await.
const sleep = ms => new Promise(r => setTimeout(r, ms));
// async generator
async function* ticker(count) {
for (let i = 1; i <= count; i++) {
await sleep(10);
yield i;
}
}
for await (const tick of ticker(3)) console.log('tick', tick);
It prints:
tick 1
tick 2
tick 3
The loop waits for each value before running the body. There is no callback and no queue to manage.
Paging over an API
The common real case is a paged endpoint. The object below pretends each page costs a network call.
// Symbol.asyncIterator on an object
const pager = {
pages: [['a', 'b'], ['c', 'd'], ['e']],
async *[Symbol.asyncIterator]() {
for (const page of this.pages) {
await sleep(10); // pretend this is a network call
yield page;
}
}
};
for await (const page of pager) console.log('page', page);
// collect everything
const all = [];
for await (const page of pager) all.push(...page);
console.log(all);
It prints:
page [ 'a', 'b' ]
page [ 'c', 'd' ]
page [ 'e' ]
[ 'a', 'b', 'c', 'd', 'e' ]
The consumer does not know or care how many pages there are. If you want everything at once, collect it in the loop.
It is a separate protocol
An async iterable does not work with plain for...of.
// for...of on an async iterable fails
try { for (const x of pager) console.log(x); }
catch (err) { console.log(err.constructor.name + ':', err.message); }
It prints:
TypeError: pager is not iterable
An object can implement both symbols if you want it to work either way, but they are different methods.
Breaking early cleans up
When you break out of the loop, the generator is closed and its finally block runs. That is where you release the connection or the file handle.
// break runs the generator's finally block
async function* withCleanup() {
try { yield 1; yield 2; yield 3; }
finally { console.log('cleanup ran'); }
}
for await (const n of withCleanup()) {
console.log('got', n);
if (n === 2) break;
}
It prints:
got 1
got 2
cleanup ran
Values 1 and 2 arrive, the break stops the loop, and cleanup runs before the loop exits. The third value is never produced.
What to remember
for await...oflooks forSymbol.asyncIterator.async function*is the easy way to write one.- A plain
for...ofloop will not accept an async iterable. breaktriggers the generatorfinallyblock, so cleanup is reliable.
Node streams already implement this protocol, so for await (const chunk of stream) works with no wrapper. Your own paged clients can look exactly the same to the code that uses them.