ESC

Type to search the knowledge base.

Async Iteration and for await...of

Async iterables, for await...of, and streaming data — how async iterators differ from Promise.all and when to use each.

advanced3 min read
  • javascript
  • async-iteration
  • for-await
  • generators

Sync iterators give you values with next(). Async iterators give you promises of { value, done }. for await...of is the syntax that consumes them — perfect for streams, paginated APIs, and anything that produces values over time without loading everything first.

Protocols

Sync Async
Iterable [Symbol.iterator]() [Symbol.asyncIterator]()
Iterator { next() → {value, done} } { next() → Promise<{value, done}> }
Loop for...of for await...of
const asyncIterable = {
  async *[Symbol.asyncIterator]() {
    yield 1;
    await new Promise((r) => setTimeout(r, 10));
    yield 2;
  },
};

for await (const n of asyncIterable) {
  console.log(n); // 1, then 2
}

Async generators

async function* is the ergonomic way to build async iterables:

async function* paginate(url) {
  let next = url;
  while (next) {
    const res = await fetch(next);
    if (!res.ok) throw new Error(String(res.status));
    const data = await res.json();
    yield* data.items; // yield each item
    next = data.nextPage ?? null;
  }
}

for await (const item of paginate('/api/items?page=1')) {
  renderRow(item);
}

Consumers pull; producers can await between yields. Backpressure is natural: the next page doesn’t fetch until the loop body finishes and requests the next value.

vs Promise.all

// all pages at once — parallel, needs all URLs upfront
const pages = await Promise.all(urls.map((u) => fetch(u).then((r) => r.json())));

// sequential stream — memory stays flat, starts rendering early
for await (const item of paginate(firstUrl)) {
  renderRow(item);
}

Use Promise.all when you want maximum concurrency and known set of tasks. Use async iteration when the sequence is open-ended or you want to process one chunk at a time.

ReadableStream as async iterable

Modern browsers let you iterate fetch bodies:

const res = await fetch('/large.txt');
const text = res.body.pipeThrough(new TextDecoderStream());

for await (const chunk of text) {
  // chunk is a string piece
  handleChunk(chunk);
}

Manual next()

const it = paginate('/api/items');
let result = await it.next();
while (!result.done) {
  console.log(result.value);
  result = await it.next();
}

for await...of also calls return() on the iterator if you break or throw — implement cleanup there for open connections.

async function* withCleanup() {
  const socket = connect();
  try {
    while (true) {
      const msg = await socket.read();
      if (msg === null) return;
      yield msg;
    }
  } finally {
    socket.close(); // runs on break / throw / normal end
  }
}

Footguns

  1. for await on a sync iterable of promises — it awaits each element. A plain array of promises works:
for await (const v of [Promise.resolve(1), Promise.resolve(2)]) {
  console.log(v); // 1, 2
}
  1. Error handling — rejections from next() reject the loop; wrap in try/catch.
  2. Don’t confuse with await in for…of — for (const p of promises) { await p } is sequential awaits on an array; not the same protocol.
  3. Node vs browser stream APIs differ slightly; check support for async iteration on your target.

Interview answer

“Async iterators implement Symbol.asyncIterator; next returns a Promise of {value, done}. for await…of consumes them. Async generators (async function*) are the usual way to build them for pagination and streams. Prefer them over Promise.all when the sequence is large or unbounded and you want pull-based processing.”

Further reading

Related guides