JavaScript Async Iterators
Learn async iterators, async generators, and for await...of for consuming asynchronous data lazily.
TL;DR
- 01Yield promises that resolve to
{ value, done }via async iterators. - 02Build async generators with
async function*toyieldvalues lazily. - 03Consume async iterables with
for await...ofas values arrive.
Tips
- 01Use async generators to wrap paginated APIs, so callers loop over pages without managing cursors manually.
- 02Prefer
for await...ofover manually callingnext()when consuming streams — it awaits values and cleans up automatically.
Warnings
- 01Awaiting each
next()call sequentially means items resolve one at a time, not all at once. - 02Forgetting that
for await...ofalso works on plain, synchronous iterables can confuse debugging of sequential async behavior.
What Async Iterators Are
Symbol.asyncIterator`next()` returns a Promise resolving to `{ value, done }`, not a plain object.
const asyncIt = {
[Symbol.asyncIterator]() {
let i = 0;
return {
next: () =>
Promise.resolve(
{ value: i++, done: i > 3 }
)
};
}
};Class-based iterableA class implements async iteration by returning `this` from `[Symbol.asyncIterator]()`.
class Logs {
constructor(lines) {
this.i = 0;
this.lines = lines;
}
[Symbol.asyncIterator]() { return this; }
next() {
const done = this.i >= this.lines.length;
const value = this.lines[this.i++];
return Promise.resolve({ value, done });
}
}Dual protocolsAn object can implement `Symbol.iterator` and `Symbol.asyncIterator` for two iteration modes.
const range = {
[Symbol.iterator]() {
let i = 0;
return {
next: () => ({ value: i++, done: i > 2 })
};
},
[Symbol.asyncIterator]() {
let i = 0;
return {
next: () => Promise.resolve(
{ value: i++, done: i > 2 }
)
};
}
};Manual next() callsCall `next()` directly and `await` each promise to drive iteration by hand.
async function drain(it) {
let result = await it.next();
while (!result.done) {
console.log(result.value);
result = await it.next();
}
}Async Generator Functions
async function*Declare an async generator by 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;
}
}yield pausesEach `yield` suspends the generator until the caller requests the next value.
async function* ticker() {
console.log('start');
yield 1;
console.log('resumed');
yield 2;
}
const t = ticker();
await t.next(); // logs 'start'
await t.next(); // logs 'resumed'Returns an iterableCalling an async generator returns an async iterable right away; the body waits.
async function* slow() {
console.log('running');
yield 1;
}
const gen = slow(); // logs nothing yet
await gen.next(); // now logs 'running'Error propagationAn error thrown inside the generator rejects the promise `next()` returns.
async function* risky() {
yield 1;
throw new Error('boom');
}
try {
for await (const v of risky()) {
console.log(v);
}
} catch (e) {
console.log(e.message); // 'boom'
}The for await...of Loop
for await...ofConsume an async iterable with `for await...of`, awaiting each value automatically.
async function run() {
for await (const items of
fetchPages('/api/items')) {
console.log(items);
}
}Waits per iterationThe loop body only runs once the currently yielded promise resolves.
async function* slowNums() {
yield 1;
await new Promise(r => setTimeout(r, 100));
yield 2;
}
for await (const n of slowNums()) {
console.log(Date.now(), n);
}Scope restriction`for await...of` only works inside an async function or a module's top level.
async function readAll(stream) {
for await (const chunk of stream) {
process(chunk);
}
}Accepts sync tooIt also accepts plain, synchronous iterables, awaiting each value for consistency.
for await (const n of [1, 2, 3]) {
console.log(n);
}
// logs: 1 2 3Cleanup on exit`break` or `return` inside the loop runs the generator's `finally` block for cleanup.
async function* withCleanup() {
try {
yield 1;
yield 2;
} finally {
console.log('cleanup');
}
}
for await (const n of withCleanup()) {
if (n === 1) break;
}
// logs 'cleanup' after breakConsuming Streams and Paginated APIs
Wrap the endpointWrap 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);
}yield* delegation`yield*` unpacks an iterable and emits each item, instead of one array.
async function* asPage() {
yield [1, 2, 3];
}
async function* asItems() {
yield* [1, 2, 3];
}
// asPage yields one array
// asItems yields 1, then 2, then 3Lazy evaluationPages fetch only when consumed; `break` stops further fetches and keeps memory flat.
let pagesFetched = 0;
async function* lazyPages() {
while (true) {
pagesFetched++;
yield pagesFetched;
}
}
for await (const page of lazyPages()) {
if (page === 2) break;
}
console.log(pagesFetched); // 2, not moreNative stream supportNode.js Readable streams implement `Symbol.asyncIterator` natively, so they work here.
for await (const chunk of
fs.createReadStream('file.txt')) {
console.log(chunk.length);
}Async vs Sync Iteration
Sync vs async generatorA sync generator yields values directly; an async one yields promises that resolve.
function* syncGen() { yield 1; yield 2; }
async function* asyncGen() {
yield 1;
yield 2;
}Matching loopPlain `for...of` cannot read an async iterable; it needs `for await...of` to unwrap values.
for (const x of asyncGen()) {}
// TypeError: not a function or its
// return value is not iterable
for await (const x of asyncGen()) {
console.log(x); // 1, then 2
}Spread limitationSpread syntax (`...`) only works with sync iterables; it cannot await an async one.
console.log([...asyncGen()]);
// TypeError: not a function or its
// return value is not iterableawait alone isn't enoughAdding `await` inside a plain `function*` is a syntax error; use `async function*`.
function* broken() {
yield 1;
await Promise.resolve(2);
// SyntaxError: await is only
// valid in async functions
}Converting sync to asyncWrap a sync iterable in an async generator, awaiting each value as it's yielded.
async function* toAsync(iterable) {
for (const value of iterable) {
yield await Promise.resolve(value);
}
}In Practice
An async generator lazily fetches pages of search results, and for await...of stops as soon as a match is found.
- 01fetchResults is an async generator that fetches one page of results at a time.
- 02yield* items emits each item individually instead of yielding whole page arrays.
- 03for await...of automatically awaits each yielded item before running the loop body.
- 04Returning early from the loop stops fetching further pages the caller no longer needs.
async function* fetchResults(query) {
let cursor = null;
do {
const res = await fetch(`/api/search?q=${query}&cursor=${cursor ?? ''}`);
const { items, nextCursor } = await res.json();
yield* items;
cursor = nextCursor;
} while (cursor);
}
async function findFirstMatch(query, predicate) {
for await (const item of fetchResults(query)) {
if (predicate(item)) return item;
}
return null;
}
const match = await findFirstMatch('laptop', item => item.price < 500);
console.log(match);
// stops fetching pages as soon as a match is foundFAQ
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.