JavaScript - Async Iterables

Overview

Estimated time: 30–40 minutes

Async iterables represent sequences whose values arrive over time. Learn how to consume them with for await...of, create them with Symbol.asyncIterator or async generator functions, and apply simple concurrency patterns.

Learning Objectives

  • Use for await...of to consume asynchronous sequences.
  • Create async iterables via Symbol.asyncIterator and async function*.
  • Bridge promises to async iteration and control cancellation.
  • Apply basic concurrency with mapping and limits.

Prerequisites

Consuming async iterables

Use for await...of inside an async function to consume an async iterable.

async function* countAsync(n, delayMs = 100) {
  for (let i = 1; i <= n; i++) {
    await new Promise(r => setTimeout(r, delayMs));
    yield i;
  }
}

(async () => {
  for await (const x of countAsync(3)) {
    console.log(x); // 1, then 2, then 3 over time
  }
})();

Creating async iterables

// Async generator (most convenient)
async function* fromArrayDelayed(arr, delayMs = 50) {
  for (const x of arr) { await new Promise(r => setTimeout(r, delayMs)); yield x; }
}

// Custom object implementing Symbol.asyncIterator
const ticker = (ticks = 3, delayMs = 100) => ({
  [Symbol.asyncIterator]() {
    let i = 0;
    return {
      async next() {
        if (i >= ticks) return { done: true };
        await new Promise(r => setTimeout(r, delayMs));
        return { value: ++i, done: false };
      },
      async return() { return { done: true }; } // handle early cancellation
    };
  }
});

Bridging promises

// for await...of can iterate a sync iterable of promises, awaiting each element
const promises = [1,2,3].map(n => new Promise(r => setTimeout(() => r(n*n), n*30)));

(async () => {
  for await (const v of promises) {
    console.log('square:', v);
  }
})();

Concurrency patterns

// Simple concurrent map with a limit (preserves input order)
async function* mapAsync(iterable, fn, concurrency = 2) {
  const it = iterable[Symbol.iterator]();
  let index = 0;
  const inFlight = new Map();

  function launch() {
    const next = it.next();
    if (next.done) return;
    const i = index++;
    inFlight.set(i, Promise.resolve(fn(next.value, i)).then(v => ({ i, v })));
  }

  // Prime the pool
  for (let k = 0; k < concurrency; k++) launch();

  while (inFlight.size) {
    const { i, v } = await Promise.race(inFlight.values());
    inFlight.delete(i);
    yield v; // yields as tasks complete (not strictly in input order)
    launch();
  }
}

Common Pitfalls

  • Top-level await: Not available in all environments; wrap in an async IIFE.
  • Backpressure: Generators that outpace consumers can buffer; design with limits or pauses.
  • Cancellation: Breaking a for await loop calls return() on the iterator if implemented.
  • Error propagation: Exceptions inside async generators reject the next next() call; wrap consumers in try/catch.

Try it

Run to see async iteration, bridging promises, and limited concurrency: