Async Iteration and for await...of
Async iterables, for await...of, and streaming data — how async iterators differ from Promise.all and when to use each.
- javascript
- async-iteration
- for-await
- generators
Sync iterators give you values with next(). Async iterators give you promises of { value, done }. for await...of is the syntax that consumes them — perfect for streams, paginated APIs, and anything that produces values over time without loading everything first.
Protocols
| Sync | Async | |
|---|---|---|
| Iterable | [Symbol.iterator]() |
[Symbol.asyncIterator]() |
| Iterator | { next() → {value, done} } |
{ next() → Promise<{value, done}> } |
| Loop | for...of |
for await...of |
const asyncIterable = {
async *[Symbol.asyncIterator]() {
yield 1;
await new Promise((r) => setTimeout(r, 10));
yield 2;
},
};
for await (const n of asyncIterable) {
console.log(n); // 1, then 2
}
Async generators
async function* is the ergonomic way to build async iterables:
async function* paginate(url) {
let next = url;
while (next) {
const res = await fetch(next);
if (!res.ok) throw new Error(String(res.status));
const data = await res.json();
yield* data.items; // yield each item
next = data.nextPage ?? null;
}
}
for await (const item of paginate('/api/items?page=1')) {
renderRow(item);
}
Consumers pull; producers can await between yields. Backpressure is natural: the next page doesn’t fetch until the loop body finishes and requests the next value.
vs Promise.all
// all pages at once — parallel, needs all URLs upfront
const pages = await Promise.all(urls.map((u) => fetch(u).then((r) => r.json())));
// sequential stream — memory stays flat, starts rendering early
for await (const item of paginate(firstUrl)) {
renderRow(item);
}
Use Promise.all when you want maximum concurrency and known set of tasks. Use async iteration when the sequence is open-ended or you want to process one chunk at a time.
ReadableStream as async iterable
Modern browsers let you iterate fetch bodies:
const res = await fetch('/large.txt');
const text = res.body.pipeThrough(new TextDecoderStream());
for await (const chunk of text) {
// chunk is a string piece
handleChunk(chunk);
}
Manual next()
const it = paginate('/api/items');
let result = await it.next();
while (!result.done) {
console.log(result.value);
result = await it.next();
}
for await...of also calls return() on the iterator if you break or throw — implement cleanup there for open connections.
async function* withCleanup() {
const socket = connect();
try {
while (true) {
const msg = await socket.read();
if (msg === null) return;
yield msg;
}
} finally {
socket.close(); // runs on break / throw / normal end
}
}
Footguns
- for await on a sync iterable of promises — it awaits each element. A plain array of promises works:
for await (const v of [Promise.resolve(1), Promise.resolve(2)]) {
console.log(v); // 1, 2
}
- Error handling — rejections from
next()reject the loop; wrap in try/catch. - Don’t confuse with await in for…of —
for (const p of promises) { await p }is sequential awaits on an array; not the same protocol. - Node vs browser stream APIs differ slightly; check support for async iteration on your target.
Interview answer
“Async iterators implement Symbol.asyncIterator; next returns a Promise of {value, done}. for await…of consumes them. Async generators (async function*) are the usual way to build them for pagination and streams. Prefer them over Promise.all when the sequence is large or unbounded and you want pull-based processing.”
Related
Further reading
Related guides
- Generatorsfunction* and yield — lazy sequences, custom iterators, two-way next(value), and how generators power async patterns.
- AbortControllerCancel fetch and other async work with AbortController and AbortSignal — timeouts, race conditions, and cleanup when components unmount.
- Array find, some, every, includesShort-circuiting array predicates: find, findIndex, some, every, and includes — when to use each and common interview traps.
- Array map, filter, reducemap, filter, and reduce as the core transform toolkit — immutability, chaining costs, reduce patterns, and when a plain loop is clearer.
- Arrow Functions Deep DiveLexical this, no arguments object, no construct, concise bodies — when arrows help and when methods and generators need classic functions.