Iterators, Generators and Async Iteration

for…of works on anything that implements the iterator protocol. Generators let you implement that protocol with a function — producing values on demand, even infinitely, even asynchronously. This lesson covers the protocol, yield, delegation, and the lazy pipelines they enable.

The iterator protocol

By hand first — then never again

// An ITERABLE has a Symbol.iterator method returning an ITERATOR:
const handMade = {
  upTo: 3,
  [Symbol.iterator]() {
    let i = 1;
    return {
      next: () => (i <= this.upTo
        ? { value: i++, done: false }   // a step
        : { value: undefined, done: true }), // the end
    };
  },
};

console.log([...handMade].join(", ")); // spread and for…of both speak this protocol
// Expected output:
// 1, 2, 3

Arrays, strings, Maps, Sets, and NodeLists all implement this — which is exactly one protocol, and why for…of works uniformly on all of them.

Generators: the protocol, written for you

yield pauses the function; next() resumes it

function* fibonacci() {
  let [a, b] = [0, 1];
  while (true) {        // an INFINITE loop — safe, because nothing runs until asked
    yield a;            // pause here, hand out a, wait to be asked again
    [a, b] = [b, a + b];
  }
}

const fib = fibonacci();
console.log(fib.next().value, fib.next().value, fib.next().value, fib.next().value);

// Expected output:
// 0 1 1 2

Calling fibonacci() runs no code — it returns a paused generator. Each .next() runs to the next yield and freezes again. Contrast with an array: a generator holds no history, only the recipe.

Lazy pipelines: cap the infinite

// take(n) stops PULLING — the generator freezes at its yield, computing nothing more.
function* take(iterable, n) {
  let count = 0;
  for (const value of iterable) {
    if (count++ >= n) return;
    yield value;
  }
}

console.log([...take(fibonacci(), 8)].join(", "));
// Expected output:
// 0, 1, 1, 2, 3, 5, 8, 13

yield* — delegation

// yield* hands control to ANOTHER generator — its yields flow through ours.
function* tree(node) {
  if (typeof node !== "object" || node === null) {
    yield node;               // a leaf: produce its value
    return;
  }
  for (const child of Object.values(node)) {
    yield* tree(child);       // recursive delegation: depth-first traversal
  }
}

const doc = { id: "root", children: [{ id: "a" }, { id: "b", children: [{ id: "ba" }] }] };
console.log([...tree(doc)].join(", "));
// Expected output:
// root, a, b, ba   (every string leaf, depth first)

This is the natural shape for walking trees, flattening structures, and composing pipelines — recursion that produces a stream instead of building an array.

Async iteration: for await…of

An async generator yields promises; for await…of awaits each value in order. This is the foundation of streaming data — event sources, paginated APIs, reading a file chunk by chunk:

async function* ticks(count) {
  for (let i = 1; i <= count; i++) {
    await new Promise((r) => setTimeout(r, 10)); // simulate an async event source
    yield `tick ${i}`;                           // produced only when the consumer asks
  }
}

for await (const message of ticks(3)) {
  console.log(message); // the loop awaits each value before the next iteration
}
// Expected output (one per 10ms):
// tick 1
// tick 2
// tick 3

Because values are produced on demand, a slow consumer applies natural backpressure: the generator simply pauses at its yield until the next pull. No buffer grows, nothing is computed early.

Practice: infinite, capped

The task

Run async/29_generators_lab.js with node. Then build function* unique(iterable) that yields each value only the first time it appears (a lazy Set filter), and chain it: [...unique(take(fibonacci(), 20))]. Predict the result before running.

The checklist

  • You can implement Symbol.iterator by hand — and explain what a generator saves you.
  • You know calling a generator function runs no code until the first next().
  • You use yield* for recursive delegation instead of manual forwarding.
  • You can explain backpressure: consumers pull, producers pause at yield.