> cat async-iterators-in-javascript.md

Async Iterators in JavaScript

📅

JavaScript gained async iterators in ES2018, letting you model streaming data with familiar for await...of syntax. They combine async/await with the iterator protocol to pull chunks of data on demand, which makes them useful for things like paginated APIs, streaming responses, or file chunks.

The AsyncIterator Protocol

The AsyncIterator interface provides the foundation for async iteration. An async iterator implements:

const asyncIterator = {
  async next() {
    // Returns Promise<{value, done}>
    return { value: someValue, done: false };
  },
  async return(value) {
    // Optional cleanup method
    return { value, done: true };
  },
  async throw(exception) {
    // Optional error handling
    return Promise.reject(exception);
  }
};

Async Generator Functions

The async function* syntax creates an async generator function that returns an async iterator. The * indicates a generator, while async makes it asynchronous:

// Yields pages from a paginated API
async function* fetchPages(endpoint, limit = 3) {
  let page = 1;
  while (true) {
    const res = await fetch(`${endpoint}?page=${page}`);
    if (!res.ok) break;
    const data = await res.json();
    if (!data.items?.length) break;
    yield data.items;
    if (page++ >= limit) break;
  }
}

Here fetchPages is an async generator: each yield pauses until the consumer asks for the next chunk.

The Async Iteration Protocols

There are two complementary protocols for async iteration, similar to their synchronous counterparts but with promises:

Async Iterable Protocol: An object implements this when it has a [Symbol.asyncIterator]() method that returns an async iterator.

Async Iterator Protocol: An object implements this when it has the following methods:

  • next(): Returns a promise that fulfils to an object with {value, done} properties
  • return(value) (optional): Returns a promise for cleanup, fulfils to {value, done: true}
  • throw(exception) (optional): Returns a promise for error handling
const asyncIterable = {
  [Symbol.asyncIterator]() {
    let count = 0;
    return {
      async next() {
        if (count < 3) {
          return { value: count++, done: false };
        }
        return { done: true };
      }
    };
  }
};

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

Syntax quick hits

  • async / await
  • for await...of
  • function*
  • Promise underpins async iteration.

Consuming with for await

for await (const items of fetchPages('/api/posts')) {
  for (const item of items) {
    console.log('post', item.id);
  }
}

for await requests values one at a time and awaits each promise the iterator returns.

Piping Streams

Node 18+ ships readable streams that are async iterable. You can chain transforms with async generators:

async function* filterLines(source, predicate) {
  for await (const chunk of source) {
    const lines = chunk.toString().split('\n');
    for (const line of lines) {
      if (predicate(line)) yield line;
    }
  }
}

const fs = await import('node:fs');
const stream = fs.createReadStream('access.log', { encoding: 'utf8' });

for await (const line of filterLines(stream, l => l.includes('ERROR'))) {
  console.log(line);
}

Interop Tips

  • Browsers: fetch responses expose body as an async iterable stream (ReadableStreamDefaultReader). Use for await on res.body.
  • Node: most modern readable streams are async iterable; legacy ones can be wrapped with Readable.from.
  • Error handling: wrap your for await in try/catch—errors from awaited operations surface there.

Async iterators give JavaScript a first-class streaming story: predictable backpressure, familiar loops, and composable pipelines without callback pyramids.