Technology · JavaScript
AdvancedJavaScript Async Iterators
Learn async iterators, async generators, and for await...of for consuming asynchronous data lazily.
TL;DR
- 01Async iterators yield promises that resolve to value and done.
- 02Use async generator functions to build async iterators easily.
- 03Loop with for await...of to consume values as they arrive.
What Async Iterators Are
- An async iterator is like a regular iterator, but
next()returns a Promise instead of a plain object.const asyncIt = { [Symbol.asyncIterator]() { let i = 0; return { next: () => Promise.resolve({ value: i++, done: i > 3 }) }; } }; - Each resolved promise still has the familiar
{ value, done }shape from sync iterators. - Async iterators exist for sources where producing the next value takes time, like a network call.
- The object becomes async-iterable by implementing
Symbol.asyncIteratorinstead ofSymbol.iterator. - An object can implement both protocols at once, offering sync and async iteration.
- Calling
next()repeatedly drives the sequence forward one awaited step at a time.
Async Generator Functions
- Declare an async generator with
async function*, combining await and yield in one body.async function* fetchPages(url) { let next = url; while (next) { const res = await fetch(next); const page = await res.json(); yield page.items; next = page.nextUrl; } } - Each
yieldpauses the generator until the consumer requests the next value. - Use
awaitfreely inside the generator body for any asynchronous step. - Calling an async generator function returns an async iterable, not a promise.
- Errors thrown inside the generator reject the promise returned by
next(). - Async generators are the easiest way to build custom async iterators in modern JavaScript.
The for await...of Loop
- Use
for await...ofto consume an async iterable, awaiting each value automatically.async function run() { for await (const items of fetchPages('/api/items')) { console.log(items); } } - The loop body only runs after each yielded promise resolves.
for await...ofcan only appear inside an async function or at the top level of a module.for await (const chunk of readableStream) { process(chunk); }- It also accepts plain (sync) iterables, awaiting each value for consistency.
breakorreturninside the loop calls the iterator'sreturn()method for cleanup.- This loop is the standard way to read Node.js streams and fetch response bodies.
Consuming Streams and Paginated APIs
- Wrap a paginated endpoint in an async generator so callers never see cursor logic.
async function* paginate(fetchPage) { let cursor = null; do { const { items, nextCursor } = await fetchPage(cursor); yield* items; cursor = nextCursor; } while (cursor); } - Use
yield*to delegate and emit each item individually instead of whole pages. - Lazy evaluation means later pages only load once earlier items are consumed.
- Node.js Readable streams implement
Symbol.asyncIteratornatively, so they work directly withfor await...of.for await (const chunk of fs.createReadStream('file.txt')) { console.log(chunk.length); } - Stop early with
breakto avoid fetching pages the caller no longer needs. - This pattern keeps memory flat regardless of total result count.
Async vs Sync Iteration
- A sync generator (
function*) yields values directly; an async generator (async function*) yields promises that resolve to values.function* syncGen() { yield 1; yield 2; } async function* asyncGen() { yield 1; yield 2; } - Sync iterators work with
for...of; async iterators needfor await...ofto unwrap each value. - Spread syntax (
...) only works with sync iterables — it cannot await an async iterator. - Mixing
awaitinside a sync generator does not make it async; the function must useasync function*. - Choose async iteration only when values genuinely arrive over time, not just to add
awaiteverywhere. - Converting a sync iterable to async iteration usually just means awaiting each value as you loop.
Tips
- 01Use async generators to wrap paginated APIs so callers can loop over pages without managing cursors or offsets manually.
- 02Prefer for await...of over manually calling next() when consuming streams, since it handles awaiting and cleanup automatically.
Warnings
- 01Awaiting each next() call sequentially means items resolve one at a time, not all at once.
- 02Forgetting that for await...of also works on plain (sync) iterables can cause confusion when debugging unexpectedly sequential async behavior.
FAQ
- Symbol.iterator defines a synchronous iterator whose next() returns {value, done} directly. Symbol.asyncIterator defines an async iterator whose next() returns a Promise that resolves to {value, done}. Use the async version whenever producing a value requires waiting, like a network request.
- Combine the async and function* keywords into async function*. Inside it, use await for asynchronous work and yield to emit values. Calling it returns an async iterable you can loop over with for await...of.
- Use for await...of when looping over an async iterable, such as an async generator or a stream of paginated results. It automatically awaits each yielded promise before running the loop body. A regular for...of loop cannot await values produced asynchronously.
- Yes — for await...of works with any iterable, sync or async, and awaits each value automatically. Given an array of promises, it awaits each one in order before continuing. This makes it useful for processing a fixed list of pending requests sequentially.
- An async generator can fetch one page, yield its items, then fetch the next page only when asked. This keeps memory usage low since pages load lazily instead of all at once. Callers just loop with for await...of and never see the pagination logic.