Async Iterators

Hard

Async iterators extend the iterator protocol to handle asynchronous data sources. Using Symbol.asyncIterator and async generator functions (async function*), you can consume paginated APIs, streaming data, and event sources with the for-await-of loop.

Interactive Visualization

for...in vs for...of

Step 1: Setup
const obj = { a: 1, b: 2 };
const arr = [10, 20, 30];
Output
No output yet
1 / 5

Key Points

  • Symbol.asyncIterator defines the async iteration protocol on objects
  • Async generators (async function*) combine generators with async/await
  • for-await-of loop consumes async iterables, awaiting each yielded promise
  • Each next() call returns a Promise<{ value, done }>
  • Ideal for paginated APIs, streaming responses, and server-sent events
  • Can be combined with AbortController for cancellable async iteration

Code Examples

Basic Async Generator

async function* asyncRange(start, end) {
  for (let i = start; i <= end; i++) {
    await new Promise(resolve => setTimeout(resolve, 100));
    yield i;
  }
}

async function main() {
  for await (const num of asyncRange(1, 5)) {
    console.log(num); // 1, 2, 3, 4, 5 (each after 100ms)
  }
}

Async generators yield values asynchronously. for-await-of automatically awaits each value.

Paginated API Iteration

async function* fetchPages(baseUrl) {
  let nextUrl = baseUrl;

  while (nextUrl) {
    const response = await fetch(nextUrl);
    const data = await response.json();

    for (const item of data.results) {
      yield item;
    }
    nextUrl = data.next;
  }
}

async function getAllUsers() {
  const users = [];
  for await (const user of fetchPages('/api/users?page=1')) {
    users.push(user);
    if (users.length >= 100) break;
  }
  return users;
}

The async generator handles pagination internally. The consumer can break early without unnecessary fetches.

Custom Async Iterable

class Timer {
  constructor(intervalMs, count) {
    this.intervalMs = intervalMs
    this.count = count
  }

  async *[Symbol.asyncIterator]() {
    for (let i = 0; i < this.count; i++) {
      await new Promise(r => setTimeout(r, this.intervalMs))
      yield { tick: i + 1, time: Date.now() }
    }
  }
}

for await (const event of new Timer(1000, 5)) {
  console.log(`Tick ${event.tick}`)
}

Objects with Symbol.asyncIterator become async iterables, consumable with for-await-of.

Cancellable Async Iteration

async function* fetchWithAbort(urls, signal) {
  for (const url of urls) {
    if (signal.aborted) return;
    const response = await fetch(url, { signal });
    yield await response.json();
  }
}

const controller = new AbortController();
setTimeout(() => controller.abort(), 5000);

try {
  for await (const data of fetchWithAbort(urls, controller.signal)) {
    console.log('Got:', data);
  }
} catch (err) {
  if (err.name === 'AbortError') console.log('Cancelled');
}

AbortController integrates with async iteration for clean cancellation.

Common Mistakes

  • Using for-of instead of for-await-of with async iterables
  • Forgetting to implement return() for cleanup when consumers break early
  • Not handling errors inside async generators
  • Creating unbounded queues without backpressure

Interview Tips

  • next() returns Promise<IteratorResult> (vs plain IteratorResult for sync)
  • Paginated API pattern is the most practical real-world use case
  • for-await-of calls return() on break, enabling resource cleanup
  • Node.js readable streams implement Symbol.asyncIterator natively

Related Concepts