Generators and Iterators: Functions That Pause

July 12, 2026 · 4 min read

Most functions run start to finish and hand back one value. Generators break that rule: they can pause in the middle, hand a value out, and later resume from the exact spot they stopped — locals and all. Understanding them explains for...of, the spread operator, lazy infinite sequences, and ultimately how async/await works.

The iterator protocol

First, the contract everything is built on. An iterator is any object with a next() method that returns { value, done }. An iterable is any object with a [Symbol.iterator] method that returns an iterator. for...of, spread (...), and destructuring all speak this protocol:

const iter = [10, 20][Symbol.iterator]();
iter.next(); // { value: 10, done: false }
iter.next(); // { value: 20, done: false }
iter.next(); // { value: undefined, done: true }

Arrays, strings, Map, and Set are all iterable — that's why you can spread or for...of them. You can make your own objects iterable by implementing [Symbol.iterator], but writing a correct iterator by hand (tracking state across calls) is fiddly. Generators do it for you.

Generator functions

A generator function — function* — returns an iterator, but its body doesn't run on call. Instead, each yield is a pause point. Calling next() runs the body up to the next yield, hands out that value, and freezes. The next next() thaws it and continues:

function* counter() {
yield 1
yield 2
yield 3
}
output
(nothing yet)

const g = counter() — calling a generator runs NO code. It hands back a paused iterator. Generators are lazy.

0 / 4
function* counter() {
  yield 1;
  yield 2;
  yield 3;
}

const g = counter();
g.next(); // { value: 1, done: false }
g.next(); // { value: 2, done: false }
g.next(); // { value: 3, done: false }
g.next(); // { value: undefined, done: true }

The revelation in the animation: calling counter() runs nothing. The body only advances when you pull the next value. Between pulls, the function sits frozen with its local state intact — that's the superpower plain functions don't have.

Because a generator returns an iterator, it's automatically iterable:

for (const n of counter()) console.log(n); // 1, 2, 3
const nums = [...counter()];               // [1, 2, 3]

Lazy and infinite sequences

Since values are produced only on demand, a generator can describe an infinite sequence without looping forever — you just stop pulling:

function* naturals() {
  let n = 1;
  while (true) yield n++;   // never terminates on its own
}

const g = naturals();
g.next().value; // 1
g.next().value; // 2   — computed only when asked

This laziness is the real value. You can express "all the natural numbers" or "every line of a huge file" as a generator and consume just the part you need, computing nothing extra. Pair it with a take(gen, k) helper and you have composable, memory-cheap pipelines.

Two-way communication

yield is also an expression — whatever you pass to next(value) becomes the result of the paused yield. So data flows in as well as out:

function* echo() {
  const received = yield "ready";
  yield `you said: ${received}`;
}

const g = echo();
g.next();          // { value: "ready", done: false }
g.next("hello");   // { value: "you said: hello", done: false }

This bidirectional channel is what makes generators powerful enough to drive async control flow.

The punchline: async/await is a generator

Here's the connection that ties it together. An async function is essentially a generator where await plays the role of yield — it pauses the function until a promise settles, then resumes with the resolved value. Before async/await was a language feature, libraries like co implemented it exactly this way: a generator that yields promises, plus a runner that calls next() each time a promise resolves.

// what async/await is doing, conceptually
function run(genFn) {
  const gen = genFn();
  function step(value) {
    const { value: promise, done } = gen.next(value);
    if (done) return Promise.resolve(promise);
    return Promise.resolve(promise).then(step); // resume when it settles
  }
  return step();
}

The pause-and-resume machinery you watched in the animation is the same machinery that lets await suspend a function without blocking the thread. async/await is generators with promises wired in and the runner built into the engine.

Wrap-up

Iterators are the { value, done } protocol that for...of and spread rely on; generators are the easy way to produce them, with the unique ability to pause at yield and resume with state intact. That gives you lazy and even infinite sequences, two-way communication — and, once promises are mixed in, async/await itself.