Async Iterators in JavaScript
JavaScript gained async iterators in ES2018, letting you model streaming data with familiar syntax. They combine for await...of with the iterator protocol to pull chunks of data on demand, which makes them useful for things like paginated APIs, streaming responses, or file chunks.async/await
The AsyncIterator Protocol
The AsyncIterator interface provides the foundation for async iteration. An async iterator implements:
const asyncIterator = {
async next() {
// Returns Promise<{value, done}>
return { value: someValue, done: false };
},
async return(value) {
// Optional cleanup method
return { value, done: true };
},
async throw(exception) {
// Optional error handling
return Promise.reject(exception);
}
};
Async Generator Functions
The syntax creates an async generator function that returns an async iterator. The async function** indicates a generator, while async makes it asynchronous:
// Yields pages from a paginated API
async function* fetchPages(endpoint, limit = 3) {
let page = 1;
while (true) {
const res = await fetch(`${endpoint}?page=${page}`);
if (!res.ok) break;
const data = await res.json();
if (!data.items?.length) break;
yield data.items;
if (page++ >= limit) break;
}
}
Here fetchPages is an async generator: each yield pauses until the consumer asks for the next chunk.
The Async Iteration Protocols
There are two complementary protocols for async iteration, similar to their synchronous counterparts but with promises:
Async Iterable Protocol: An object implements this when it has a [Symbol.asyncIterator]() method that returns an async iterator.
Async Iterator Protocol: An object implements this when it has the following methods:
next(): Returns a promise that fulfils to an object with{value, done}propertiesreturn(value)(optional): Returns a promise for cleanup, fulfils to{value, done: true}throw(exception)(optional): Returns a promise for error handling
const asyncIterable = {
[Symbol.asyncIterator]() {
let count = 0;
return {
async next() {
if (count < 3) {
return { value: count++, done: false };
}
return { done: true };
}
};
}
};
for await (const value of asyncIterable) {
console.log(value); // 0, 1, 2
}
Syntax quick hits
-
async/await -
for await...of -
function* underpins async iteration.Promise
Consuming with for await
for await (const items of fetchPages('/api/posts')) {
for (const item of items) {
console.log('post', item.id);
}
}
requests values one at a time and awaits each promise the iterator returns.for await
Piping Streams
Node 18+ ships readable streams that are async iterable. You can chain transforms with async generators:
async function* filterLines(source, predicate) {
for await (const chunk of source) {
const lines = chunk.toString().split('\n');
for (const line of lines) {
if (predicate(line)) yield line;
}
}
}
const fs = await import('node:fs');
const stream = fs.createReadStream('access.log', { encoding: 'utf8' });
for await (const line of filterLines(stream, l => l.includes('ERROR'))) {
console.log(line);
}
Interop Tips
- Browsers: fetch responses expose
bodyas an async iterable stream (ReadableStreamDefaultReader). Usefor awaitonres.body. - Node: most modern readable streams are async iterable; legacy ones can be wrapped with
Readable.from. - Error handling: wrap your
for awaitin try/catch—errors from awaited operations surface there.
Async iterators give JavaScript a first-class streaming story: predictable backpressure, familiar loops, and composable pipelines without callback pyramids.