Technology · JavaScript

Advanced

JavaScript Async Iterators

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

TL;DR
  1. 01Async iterators yield promises that resolve to value and done.
  2. 02Use async generator functions to build async iterators easily.
  3. 03Loop with for await...of to consume values as they arrive.

What Async Iterators Are

  • An async iterator is like a regular iterator, but next() returns a Promise instead of a plain object.
    const asyncIt = {
      [Symbol.asyncIterator]() {
        let i = 0;
        return {
          next: () => Promise.resolve({ value: i++, done: i > 3 })
        };
      }
    };
    
  • Each resolved promise still has the familiar { value, done } shape from sync iterators.
  • Async iterators exist for sources where producing the next value takes time, like a network call.
  • The object becomes async-iterable by implementing Symbol.asyncIterator instead of Symbol.iterator.
  • An object can implement both protocols at once, offering sync and async iteration.
  • Calling next() repeatedly drives the sequence forward one awaited step at a time.

Async Generator Functions

  • Declare an async generator with async function*, 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;
      }
    }
    
  • Each yield pauses the generator until the consumer requests the next value.
  • Use await freely inside the generator body for any asynchronous step.
  • Calling an async generator function returns an async iterable, not a promise.
  • Errors thrown inside the generator reject the promise returned by next().
  • Async generators are the easiest way to build custom async iterators in modern JavaScript.

The for await...of Loop

  • Use for await...of to consume an async iterable, awaiting each value automatically.
    async function run() {
      for await (const items of fetchPages('/api/items')) {
        console.log(items);
      }
    }
    
  • The loop body only runs after each yielded promise resolves.
  • for await...of can only appear inside an async function or at the top level of a module.
    for await (const chunk of readableStream) {
      process(chunk);
    }
    
  • It also accepts plain (sync) iterables, awaiting each value for consistency.
  • break or return inside the loop calls the iterator's return() method for cleanup.
  • This loop is the standard way to read Node.js streams and fetch response bodies.

Consuming Streams and Paginated APIs

  • 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);
    }
    
  • Use yield* to delegate and emit each item individually instead of whole pages.
  • Lazy evaluation means later pages only load once earlier items are consumed.
  • Node.js Readable streams implement Symbol.asyncIterator natively, so they work directly with for await...of.
    for await (const chunk of fs.createReadStream('file.txt')) {
      console.log(chunk.length);
    }
    
  • Stop early with break to avoid fetching pages the caller no longer needs.
  • This pattern keeps memory flat regardless of total result count.

Async vs Sync Iteration

  • A sync generator (function*) yields values directly; an async generator (async function*) yields promises that resolve to values.
    function* syncGen() { yield 1; yield 2; }
    async function* asyncGen() { yield 1; yield 2; }
    
  • Sync iterators work with for...of; async iterators need for await...of to unwrap each value.
  • Spread syntax (...) only works with sync iterables — it cannot await an async iterator.
  • Mixing await inside a sync generator does not make it async; the function must use async function*.
  • Choose async iteration only when values genuinely arrive over time, not just to add await everywhere.
  • Converting a sync iterable to async iteration usually just means awaiting each value as you loop.
Tips
  1. 01Use async generators to wrap paginated APIs so callers can loop over pages without managing cursors or offsets manually.
  2. 02Prefer for await...of over manually calling next() when consuming streams, since it handles awaiting and cleanup 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 (sync) iterables can cause confusion when debugging unexpectedly sequential async behavior.
FAQ
JavaScript Fetch APIJavaScript Call Apply Bind