Async Generators and Practical Patterns

An async generator combines a generator with async/await. It produces its values asynchronously, and the caller reads them with for await...of.

Async Generators

We write an async generator with async function*. We can await inside it, and the caller iterates it with for await...of:

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

// Use for-await-of to iterate
async function run() {
  for await (const num of asyncRange(1, 5)) {
    console.log(num);  // Logs 1, 2, 3, 4, 5 with 100ms delays
  }
}

run();

Async Data Stream Example

async function* fetchPages(baseUrl, maxPages) {
  for (let page = 1; page <= maxPages; page++) {
    const response = await fetch(`${baseUrl}?page=${page}`);
    const data = await response.json();

    if (data.items.length === 0) return;

    yield data.items;
  }
}

async function getAllItems() {
  const allItems = [];
  for await (const items of fetchPages("/api/data", 10)) {
    allItems.push(...items);
  }
  return allItems;
}

Practical Use Cases

Tree Traversal

function* walkTree(node) {
  yield node.value;
  for (const child of node.children || []) {
    yield* walkTree(child);
  }
}

const tree = {
  value: "root",
  children: [
    { value: "a", children: [{ value: "a1" }, { value: "a2" }] },
    { value: "b" },
  ],
};

console.log([...walkTree(tree)]); // ['root', 'a', 'a1', 'a2', 'b']

ID Generator

function* idGenerator(prefix = "id") {
  let id = 1;
  while (true) {
    yield `${prefix}_${id++}`;
  }
}

const userIds = idGenerator("user");
console.log(userIds.next().value); // "user_1"
console.log(userIds.next().value); // "user_2"

Lazy Evaluation Pipeline

function* range(start, end, step = 1) {
  for (let i = start; i <= end; i += step) {
    yield i;
  }
}

function* map(iterable, fn) {
  for (const item of iterable) {
    yield fn(item);
  }
}

function* filter(iterable, predicate) {
  for (const item of iterable) {
    if (predicate(item)) yield item;
  }
}

function* take(iterable, n) {
  let count = 0;
  for (const item of iterable) {
    if (count++ >= n) return;
    yield item;
  }
}

// Lazily process a large range
const result = filter(
  map(range(1, 1000000), (x) => x * 2),
  (x) => x % 10 === 0,
);

// Only compute what we need
console.log([...take(result, 5)]); // [10, 20, 30, 40, 50]

Summary

Concept Description
Iterable Object with [Symbol.iterator]() method
Iterator Object with next() returning { value, done }
Generator Function that can pause (yield) and resume
yield* Delegate to another iterator/generator
Async Generator Generator with async function* and for await...of