This article is published in English.
JavaScript Generators Explained: Pausable Functions and Lazy Iteration
Learn how JavaScript generator functions pause and resume execution, implement the iterator protocol, and enable lazy, memory-efficient data streaming.
Every function you have written up to now obeys one rule: you call it, it executes from start to finish, it returns a single time, and that is the end of the story. Generators quietly break that rule, and the break turns out to matter a lot in practice: a generator function can suspend itself partway through, pass a value back to whoever invoked it, and later resume from that exact spot, as though no time had passed in between.
The Basic Mechanism
You declare a generator with function*, and instead of return it uses yield to hand out values one at a time:
function* countUp() {
console.log("starting");
yield 1;
console.log("resumed after first yield");
yield 2;
console.log("resumed after second yield");
yield 3;
console.log("done");
}
const counter = countUp();
console.log(counter.next()); // "starting" logs, then { value: 1, done: false }
console.log(counter.next()); // "resumed after first yield" logs, then { value: 2, done: false }
console.log(counter.next()); // "resumed after second yield" logs, then { value: 3, done: false }
console.log(counter.next()); // "done" logs, then { value: undefined, done: true }
Invoking countUp() does not execute any of the function body. It hands you back a generator object, a dormant construct that has not yet begun running. Every subsequent call to .next() picks up execution right where it previously halted, runs forward until it hits the next yield, and freezes again, returning whatever value was yielded. All of the function's internal state, its variables, its position inside a loop, anything at all, survives across these pauses intact, exactly as though the function had never actually been interrupted.
Why This Is Genuinely Different From Just Returning an Array
A natural objection is: why not simply construct an array and hand that back instead? This is where generators earn their keep, since they produce output lazily, item by item, only as requested, rather than computing the whole result set in advance.
function* readLargeFileLineByLine(filePath) {
const fileHandle = fs.openSync(filePath, "r");
let position = 0;
let leftover = "";
while (true) {
const buffer = Buffer.alloc(1024);
const bytesRead = fs.readSync(fileHandle, buffer, 0, 1024, position);
if (bytesRead === 0) break; position += bytesRead;
const lines = (leftover + buffer.toString("utf8", 0, bytesRead)).split("\n");
leftover = lines.pop();
for (const line of lines) yield line;
}
fs.closeSync(fileHandle);
}for (const line of readLargeFileLineByLine("access.log")) {
if (line.includes("500")) console.log(line);
// only reads and holds one small chunk of the file in memory at a time
}
Had this been written to assemble an array of every line up front, a multi-gigabyte log file would have to be read into memory in its entirety before you could touch a single line of it, which is the same problem that streaming approaches are built to avoid. A generator avoids it through a different mechanism: each line is only computed the moment something requests it through .next(), and if the surrounding for...of loop breaks out early, for instance once it has found the match it was looking for, the generator never bothers computing anything past that point.
Generators Implement the Iterator Protocol, Which Is Why for...of Just Works
Constructs like for...of, array destructuring, and the spread operator all operate on anything that satisfies the iterator protocol, meaning any object exposing a next() method that returns { value, done }. A generator object automatically fits that shape without any extra effort on your part, which explains why you can iterate over one directly with no additional wiring:
function* pageThroughResults(fetchPage) {
let page = 1;
while (true) {
const results = fetchPageSync(fetchPage, page);
if (results.length === 0) return;
yield* results; // delegates to another iterable, yielding each of its values
page++;
}
}
The yield* syntax delegates to another iterable, forwarding each of its values in sequence. That is how a generator can turn a series of paginated results into one uninterrupted stream of individual items, without the code consuming it ever needing to know that pagination was happening underneath.
Two-Way Communication: .next() Can Send Values In, Not Just Pull Them Out
There's a lesser-known side to generators: yield isn't just a way to emit values, it's an expression, and whatever you pass into the following .next() call becomes the result of that expression. That means data can travel back into a paused function, not merely out of it.
function* priceNegotiation() {
const offer1 = yield "What's your offer?";
const offer2 = yield `I can't do ${offer1}, how about a counter?`;
return `Final: ${offer2}`;
}
const negotiation = priceNegotiation();
console.log(negotiation.next().value); // "What's your offer?"
console.log(negotiation.next(50).value); // "I can't do 50, how about a counter?"
console.log(negotiation.next(80).value); // "Final: 80"
Every time you call .next(value), the generator wakes up and the paused yield line evaluates to whatever value you supplied. Having a function that can suspend, wait to receive fresh input from the caller, and then continue working with that input is an unusual capability in ordinary function design. It's precisely this mechanism that early JavaScript async libraries leaned on to fake async/await before the language supported it natively: a scheduler would resolve a promise, then feed its result into the generator via .next(), repeating this at each yield until every asynchronous step had completed.
Why This Is the Actual Foundation Worth Knowing
Generators are far from a syntactic oddity you can safely ignore. They're the mechanism that async/await was eventually layered on top of, since "suspend execution here and pick it back up later with some value" is exactly the behavior awaiting a promise requires. They also explain why for...of, spread syntax, and destructuring behave consistently across arrays, strings, Map instances, and custom objects, since the iterator protocol that generators implement is identical to the one those built-in types already rely on. Once you internalize the real mental model, a function capable of pausing and resuming while exchanging values at each pause point, lazy evaluation, custom iteration logic, and the internal workings of async/await stop feeling like three unrelated features. They turn out to be a single underlying pattern showing up in three different contexts.