Concept
The beginner framing: a generator function (function*) is a function that can pause itself with yield, returning a value out to whoever called it, and later, resume exactly where it left off, as many times as you like.
The precise mental model: calling a generator function does not run its body at all, it returns a special Generator object (which is also an Iterator) immediately, without executing a single line. The body only starts running when you call .next() on that generator, and it runs until it hits a yield (pausing there and returning { value, done: false }) or a return/falls off the end (returning { value, done: true }).
function* countUp() {
console.log("starting");
yield 1;
console.log("resumed after first yield");
yield 2;
console.log("resumed after second yield");
return 3;
}
const gen = countUp(); // NOTHING logged yet, the body hasn't run
console.log(gen.next()); // logs "starting", returns { value: 1, done: false }
console.log(gen.next()); // logs "resumed after first yield", returns { value: 2, done: false }
console.log(gen.next()); // logs "resumed after second yield", returns { value: 3, done: true }
console.log(gen.next()); // { value: undefined, done: true }, already finished, stays finishedThis "pause and resume, on demand" capability is genuinely unique among JavaScript functions, a normal function runs start-to-finish, uninterruptible, the moment you call it. A generator is the one exception, and it's why generators are the actual foundation async/await was built on (a promise-driving loop calling .next() every time the previous yielded promise resolves, exactly the run() helper built in Async/Await's Implement It Yourself section).
.next(value) sends a value INTO the generator
function* echo() {
const x = yield "first"; // pauses here, returning "first" out
console.log("received:", x); // resumes here when .next(someValue) is called
const y = yield "second";
console.log("received:", y);
}
const gen = echo();
console.log(gen.next()); // { value: "first", done: false }, x not assigned yet
console.log(gen.next("hello")); // logs "received: hello", then { value: "second", done: false }
console.log(gen.next("world")); // logs "received: world", then { value: undefined, done: true }Communication flows both directions: yield expr sends a value out; the argument to the next .next(value) call becomes that yield expression's result, sent in. This bidirectional channel is what makes generators powerful enough to drive async control flow, not just produce sequences.
Generators ARE iterators, this is why for...of works on them directly
function* range(start, end) {
for (let i = start; i <= end; i++) {
yield i;
}
}
for (const n of range(1, 5)) {
console.log(n); // 1, 2, 3, 4, 5, for...of calls .next() automatically until done
}
console.log([...range(1, 3)]); // [1, 2, 3], spread also uses the iterator protocolAny object implementing the iterator protocol (a [Symbol.iterator] method returning an object with .next()) works with for...of, spread, and destructuring, generators are simply the easiest way to implement that protocol, since function* handles all the {value, done} bookkeeping for you.
yield* delegates to another iterable
function* inner() {
yield "a";
yield "b";
}
function* outer() {
yield 1;
yield* inner(); // delegates, yields EACH of inner's values in turn
yield 2;
}
console.log([...outer()]); // [1, "a", "b", 2]Try It
Predict what this logs, step by step, before running it.