Async iteration and async generators

JavaScript · Advanced · 12 min

Paginated APIs, streams, file lines and event sources are all naturally async sequences.

Some sequences produce values over time: pages from an API, chunks from a network stream, lines from a file. Async iteration is the iteration protocol with promises: an async iterable has a [Symbol.asyncIterator]() method whose iterator's next() returns a promise of { value, done }.

Consume one with `for await...of` inside an async function or module:

for await (const chunk of stream) process(chunk);

Each iteration waits for the next value; break, return or a thrown error close the iterator so the source can clean up. for await also accepts ordinary iterables of promises, awaiting each element in turn.

The easiest way to produce one is an async generator (async function*): you can await inside it and yield values as they become available. The consumer pulls values one at a time, so the generator only does work on demand — natural backpressure. Classic example: hiding pagination behind a flat stream of items, so callers never see page tokens.

Where you'll meet async iterables:

  • Node.js readable streams, readline interfaces and fs.promises.opendir.
  • Web ReadableStreams in runtimes that support async-iterating them (Node.js, Deno and recent browsers).
  • events.on(emitter, "data") in Node.js.

Array.fromAsync(asyncIterable) collects everything into an array.

Note that for await processes items sequentially. If items are independent and you want concurrency, gather them and use Promise.all — with a limit if there are many.

Example

const sleep = ms => new Promise(r => setTimeout(r, ms));

// Fake paginated API: 3 pages of 2 items
async function fetchPage(cursor = 0) {
  await sleep(20);
  const items = [`item${cursor * 2}`, `item${cursor * 2 + 1}`];
  return { items, next: cursor < 2 ? cursor + 1 : null };
}

async function* allItems() {
  let cursor = 0;
  while (cursor !== null) {
    const page = await fetchPage(cursor);   // fetched only when needed
    yield* page.items;
    cursor = page.next;
  }
}

for await (const item of allItems()) {
  console.log(item);
  if (item === "item3") break;               // stops: page 3 is never fetched
}

const everything = await Array.fromAsync(allItems());
console.log(everything.length);              // 6

Remember

  • Async iterables return promises from next(); consume with for await...of
  • async function* can await and yield — great for pagination and streams
  • Iteration is sequential and pull-based; use Promise.all for concurrency

Watch out for

  • Using for await over independent requests and getting serial instead of parallel execution.

Check yourself

  1. What does calling an async generator function return?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for-await...of