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.
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 completionfunction* (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 iterableNotice 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, 3yield 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 memoryBecause 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?