Generators — pausable functions with yield

A normal function runs start to finish in one go, with no way to pause partway through and resume later — a generator function genuinely can, and that one difference is what makes it possible to implement the entire iterator protocol from the previous lesson in about five lines instead of a hand-written next() method.

Intermediate

4 min read

The core difference: function* can pause mid-execution, an ordinary function cannot

function* countUp() {
  console.log("starting");
  yield 1;               // PAUSES here, returns 1 — execution genuinely stops mid-function
  console.log("resumed after first yield");
  yield 2;               // pauses AGAIN
  console.log("resumed after second yield");
  yield 3;
}
 
const gen = countUp();     // calling a generator function does NOT run its body at all yet
gen.next(); // logs "starting", returns { value: 1, done: false } — paused at the FIRST yield
gen.next(); // logs "resumed after first yield", returns { value: 2, done: false }
gen.next(); // logs "resumed after second yield", returns { value: 3, done: false }
gen.next(); // returns { value: undefined, done: true } — the function has now run to completion

function* (a generator function) doesn't run its body when called — it returns a generator object immediately, and the body only actually executes, piece by piece, as .next() is called repeatedly. Each yield genuinely pauses execution at that exact point, returning a value, and the next call to .next() resumes execution from exactly where it paused — including any local variables, which stay alive across the pause, something a normal function has no way to do at all once it returns.

Why a generator is automatically iterable — the exact shape the previous lesson required

const gen = countUp();
gen.next(); // { value: 1, done: false } — SAME shape the iterator protocol requires
 
for (const n of countUp()) { console.log(n); } // 1, 2, 3 — generators are AUTOMATICALLY iterable

Notice that .next()'s return shape — { value, done } — is exactly the iterator protocol's required shape from the previous lesson, and a generator object also implements Symbol.iterator itself (returning itself), which is why every generator is automatically, immediately usable with for...of, spread, and destructuring, with zero extra code. This is the real, practical payoff: implementing the range example from the previous lesson by hand needed a manually-written next() method tracking state explicitly; the same thing as a generator is dramatically shorter.

// The SAME range iterator from the previous lesson, as a generator — far less code
function* range(from, to) {
  for (let i = from; i <= to; i++) {
    yield i;
  }
}
for (const n of range(1, 3)) { console.log(n); } // 1, 2, 3

yield inside a loop: lazy, on-demand values, not a pre-built array

function* infiniteCounter() {
  let n = 0;
  while (true) { yield n++; } // an INFINITE sequence — impossible to represent as a real, finished array
 
// Safe, because values are only produced ON DEMAND, one at a time, as .next() is actually called
const counter = infiniteCounter();
counter.next().value; // 0
counter.next().value;  // 1
counter.next().value;   // 2 — could continue forever, without ever running out of memory

Because a generator only computes the next value when .next() is actually called — not all values upfront — it can represent a genuinely infinite sequence safely, something a real, materialized array structurally cannot do (an infinite array would need infinite memory). This "compute lazily, on demand" property is the same core idea this platform's Node.js domain covered for streams — processing data incrementally instead of requiring the whole thing to exist in memory at once — applied here to sequences of values instead of file/network data.

yield returning a value back INTO the generator: two-way communication

function* echo() {
  const received = yield "ready?"; // yield can also RECEIVE a value, passed via the NEXT .next() call
  console.log("received:", received);
}
const gen = echo();
gen.next();          // runs to the yield, returns { value: "ready?", done: false }
gen.next("yes!");     // RESUMES the paused yield expression, with "yes!" as its value — logs "received: yes!"

yield isn't purely one-directional — the value passed to a subsequent .next(value) call becomes the result of the yield expression that was paused, letting the caller send data back into the generator at the exact point it resumes. This genuine two-way communication is a real, distinguishing capability generators have that a plain iterator (hand-written next() method) doesn't get for free — it's less commonly used than simple value-producing generators, but it's the mechanism that made generators a real, serious candidate for implementing async control flow before async/await existed as dedicated syntax.

Further reading

Check your understanding

A quick comprehension check — not tracked, not graded, just for you.

1. What happens when a generator function is called, compared to an ordinary function?

2. Why is a generator automatically usable with for...of, with zero extra code?

3. Why can a generator safely represent an infinite sequence, when a real array structurally cannot?