JavaScript Async Iterators

Learn async iterators, async generators, and for await...of for consuming asynchronous data lazily.

TL;DR

  1. 01Yield promises that resolve to { value, done } via async iterators.
  2. 02Build async generators with async function* to yield values lazily.
  3. 03Consume async iterables with for await...of as values arrive.

Tips

  1. 01Use async generators to wrap paginated APIs, so callers loop over pages without managing cursors manually.
  2. 02Prefer for await...of over manually calling next() when consuming streams — it awaits values and cleans up automatically.

Warnings

  1. 01Awaiting each next() call sequentially means items resolve one at a time, not all at once.
  2. 02Forgetting that for await...of also 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 iterable

    A 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 protocols

    An 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() calls

    Call `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 pauses

    Each `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 iterable

    Calling 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 propagation

    An 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...of

    Consume 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 iteration

    The 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 too

    It also accepts plain, synchronous iterables, awaiting each value for consistency.

    for await (const n of [1, 2, 3]) {
      console.log(n);
    }
    // logs: 1 2 3
    Cleanup 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 break

Consuming Streams and Paginated APIs

    Wrap the endpoint

    Wrap 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 3
    Lazy evaluation

    Pages 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 more
    Native stream support

    Node.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 generator

    A 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 loop

    Plain `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 limitation

    Spread 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 iterable
    await alone isn't enough

    Adding `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 async

    Wrap 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

FAQ