Learn async iterators, async generators, and for await...of for consuming asynchronous data lazily.
{ value, done } via async iterators.async function* to yield values lazily.for await...of as values arrive.for await...of over manually calling next() when consuming streams — it awaits values and cleans up automatically.next() call sequentially means items resolve one at a time, not all at once.for await...of also works on plain, synchronous iterables can confuse debugging of sequential async behavior.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 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'
}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 breakWrap 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);
}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);
}
}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.
Streaming Paginated Search Results
An async generator lazily fetches pages of search results, and for await...of stops as soon as a match is found.
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 foundAsync generators plus for await...of stream results lazily — you only fetch as many pages as you actually need.