JavaScript Async Iterators

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

TL;DR

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

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);
      }
    }

Tips

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

Warnings

  1. Awaiting each next() call sequentially means items resolve one at a time, not all at once.
  2. Forgetting that for await...of also works on plain, synchronous iterables can confuse debugging of sequential async behavior.

In Practice

FAQ