Generators
A normal function runs from top to bottom and hands back a single value (or nothing). Once it returns, it’s done — there’s no way to peek at the work in progress.
Generators break that rule. A generator can produce a whole series of values, one at a time, pausing between each and only continuing when you ask for more. That pausing is the whole point: it lets a function surrender control mid-execution, wait, and pick up exactly where it left off. They pair naturally with iterables, which makes them a clean way to describe data that arrives piece by piece.
Generator functions
To define a generator you use a special syntax, function* — the “generator function”:
function* countByTens() {
yield 10;
yield 20;
return 30;
}
The star is what makes it a generator. Everything else looks like an ordinary function, but the behavior when you call it is completely different.
Calling a regular function runs its body immediately. Calling a generator function runs none of the body. Instead it hands back a special object — the generator object — whose job is to drive the execution on demand.
function* countByTens() {
yield 10;
yield 20;
return 30;
}
// calling the generator function creates a generator object
let generator = countByTens();
alert(generator); // [object Generator]
At this point not a single line inside countByTens has run. The generator object just sits there, paused before the first line, waiting.
The primary method of a generator is next(). Each call runs the body up to the next yield <value> statement (the value is optional; leave it out and you yield undefined). At that point execution freezes and the yielded value flows back out to the caller.
The return of next() is always an object with two fields:
value— the value that was yielded.done—falsewhile the body has more to run,trueonce it has finished.
Here’s the first call in action:
function* countByTens() {
yield 10;
yield 20;
return 30;
}
let generator = countByTens();
let one = generator.next();
alert(JSON.stringify(one)); // {value: 10, done: false}
So far we’ve pulled out one value. The generator is now paused on the line right after yield 10, holding its place.
Call next() again and the body resumes, running until it hits the next yield:
let two = generator.next();
alert(JSON.stringify(two)); // {value: 20, done: false}
A third call reaches the return. That both produces the final value and finishes the generator, so done flips to true:
let three = generator.next();
alert(JSON.stringify(three)); // {value: 30, done: true}
The generator is spent. The done: true tells you it’s over, and value: 30 is the final result carried by the return.
Once a generator is finished, further next() calls are pointless. They keep returning the same idle result, {value: undefined, done: true}.
Drive it yourself. Each press of next() advances the same generator by one step; watch done flip to true on the return, then stay idle:
Generators are iterable
The next() method returning {value, done} objects is exactly the shape an iterator has. So generators are iterable, and you can walk them with for..of:
function* countByTens() {
yield 10;
yield 20;
return 30;
}
let generator = countByTens();
for(let value of generator) {
alert(value); // 10, then 20
}
Cleaner than chaining .next().value by hand.
But look closely at the output: it prints 10 and 20, then stops. The 30 never appears. That’s the trap from the callout earlier — for..of reads until it sees done: true, and it discards the value attached to that final step. Since 30 came from return (which sets done: true), the loop skips it.
If you want every number to show up in the loop, hand them all over with yield and don’t rely on return to deliver a value:
function* countByTens() {
yield 10;
yield 20;
yield 30;
}
let generator = countByTens();
for(let value of generator) {
alert(value); // 10, then 20, then 30
}
Because generators are iterable, everything that works on iterables works on them, including the spread syntax ...:
function* countByTens() {
yield 10;
yield 20;
yield 30;
}
let sequence = [0, ...countByTens()];
alert(sequence); // 0, 10, 20, 30
Here ...countByTens() pulls every yielded value out of the generator and drops them into the array. (More on spread in Rest parameters and spread.)
Using generators for iterables
Back in the Iterables chapter we built a span object that yields numbers start..end. Making it iterable took a fair amount of boilerplate — a Symbol.iterator method that returns an iterator object with its own state and a next():
let span = {
start: 2,
end: 6,
// for..of calls this once, at the start
[Symbol.iterator]() {
// it returns the iterator object:
// from here on, for..of talks only to this object, asking it for next values
return {
current: this.start,
last: this.end,
// next() is called on every iteration by for..of
next() {
// it should return the value wrapped as {done:.., value:..}
if (this.current <= this.last) {
return { done: false, value: this.current++ };
} else {
return { done: true };
}
}
};
}
};
// iterating over span gives numbers from span.start to span.end
alert([...span]); // 2,3,4,5,6
You can throw all of that away. Make Symbol.iterator a generator method, and the generator object it returns already satisfies the iterator protocol:
let span = {
start: 2,
end: 6,
*[Symbol.iterator]() { // shorthand for [Symbol.iterator]: function*()
for(let value = this.start; value <= this.end; value++) {
yield value;
}
}
};
alert( [...span] ); // 2,3,4,5,6
This works because span[Symbol.iterator]() now returns a generator, and a generator is precisely what for..of wants:
- it has a
.next()method, - which returns values shaped like
{value: ..., done: true/false}.
That’s no accident. Generators were added to the language alongside iterators specifically so that writing iterators would stop being tedious. The state (current, tracking where we are) that we managed by hand in the first version is now held automatically by the generator’s paused execution.
Pick any bounds and spread the object — the tiny generator method is the entire iterator:
Generator composition
Composition lets one generator embed another so that the inner generator’s values flow straight through, as if the outer one produced them itself.
Start with a small building block that yields a run of numbers:
function* emitRange(start, end) {
for (let i = start; i <= end; i++) yield i;
}
Now suppose we want a longer sequence built from three ranges of character codes:
- the digits
0..9(codes 48–57), - the uppercase letters
A..Z(codes 65–90), - the lowercase letters
a..z(codes 97–122).
Chaining these could power a base62 alphabet — the 0..9A..Za..z set behind many short-ID and short-URL schemes. First, let’s just produce the codes.
In a regular function you’d call each helper, collect its results into arrays, and concatenate them at the end. Generators offer a dedicated operator for this: yield*.
function* emitRange(start, end) {
for (let i = start; i <= end; i++) yield i;
}
function* base62Codes() {
// 0..9
yield* emitRange(48, 57);
// A..Z
yield* emitRange(65, 90);
// a..z
yield* emitRange(97, 122);
}
let str = '';
for(let code of base62Codes()) {
str += String.fromCharCode(code);
}
alert(str); // 0..9A..Za..z
yield* delegates to another generator. It iterates the inner generator and forwards every value it yields straight out through the outer one, as if those values had been written with plain yield in the outer body.
The result is identical to inlining every helper’s loop directly into the outer generator:
function* emitRange(start, end) {
for (let i = start; i <= end; i++) yield i;
}
function* base62Inline() {
// yield* emitRange(48, 57);
for (let i = 48; i <= 57; i++) yield i;
// yield* emitRange(65, 90);
for (let i = 65; i <= 90; i++) yield i;
// yield* emitRange(97, 122);
for (let i = 97; i <= 122; i++) yield i;
}
let str = '';
for(let code of base62Inline()) {
str += String.fromCharCode(code);
}
alert(str); // 0..9A..Za..z
Both versions produce the same string. Composition is the natural way to splice one generator’s output into another, and it’s memory-friendly: values pass through one at a time, so nothing is buffered into intermediate arrays.
Here that same composed generator feeds a real task — pulling random characters from the combined pool to build a random short ID:
“yield” is a two-way street
Up to now generators have looked like a slick way to describe iterables. But there’s more to them, and it comes from yield working in both directions.
yield doesn’t only push a value out. It can also receive a value coming back in. When you call generator.next(arg) with an argument, that arg becomes the value that the paused yield expression evaluates to.
function* gen() {
// send a question out to the caller, then wait for an answer
let result = yield "Days in a week?"; // (*)
alert(result);
}
let generator = gen();
let question = generator.next().value; // <-- yield hands the value out
generator.next(7); // --> pass the answer back into the generator
Walk through what happens:
- The first
generator.next()must be called with no argument — if you pass one it’s ignored, because there’s no suspendedyieldyet to receive it. It runs the body up toyield "Days in a week?", freezes on line(*), and returns that string asvalue. - That string lands in the caller’s
questionvariable. - When the caller runs
generator.next(7), the generator wakes up and7becomes the value of theyieldexpression, soresultis set to7.
The caller isn’t forced to answer right away. The generator stays parked until the next next(), however long that takes:
// resume the generator later
setTimeout(() => generator.next(7), 1000);
Regular functions can’t do this — once they start, you can’t feed them new data mid-run. A generator and its caller can trade values back and forth throughout the run.
Here’s a longer exchange to make the rhythm clear:
function* gen() {
let ask1 = yield "Days in a week?";
alert(ask1); // 7
let ask2 = yield "Sides on a hexagon?"
alert(ask2); // 6
}
let generator = gen();
alert( generator.next().value ); // "Days in a week?"
alert( generator.next(7).value ); // "Sides on a hexagon?"
alert( generator.next(6).done ); // true
Step by step:
- The first
.next()starts execution and runs to the firstyield. - That first yielded string goes out to the caller.
.next(7)sends7back in as the result of the firstyield, then resumes.- Execution reaches the second
yield, whose value becomes the result of thatnext()call. .next(6)sends6in as the result of the secondyield, resumes, runs off the end of the function, and reportsdone: true.
It’s a game of ping-pong. Every next(value) after the first does two things at once: it delivers value as the result of the current yield, and it collects whatever the next yield produces.
Play the caller’s side. The generator asks a question with yield; your answer goes back in through next(value) and becomes what that yield evaluated to — which even lets it react to what you typed:
generator.throw
The caller can pass a value into a paused yield. It can also do the opposite of a value — it can throw an error at that point. An error is just another kind of result, after all.
Call generator.throw(err) and err is raised right at the line where the generator is suspended on yield:
function* gen() {
try {
let result = yield "Days in a week?"; // (1)
alert("execution never reaches here, the exception is thrown above");
} catch(e) {
alert(e); // shows the error
}
}
let generator = gen();
let question = generator.next().value;
generator.throw(new Error("No answer found in the lookup table")); // (2)
The error injected at line (2) surfaces inside the generator at line (1), exactly where it’s paused. Because that yield sits inside a try..catch, the generator catches its own error and reports it.
If the generator doesn’t catch the error, it behaves like any uncaught exception: it propagates out of the generator and back into the calling code. The call that started it is the line with generator.throw, labelled (2) — so you can wrap that call in try..catch and handle it there instead:
function* generate() {
let result = yield "Days in a week?"; // Error surfaces on this line
}
let generator = generate();
let question = generator.next().value;
try {
generator.throw(new Error("No answer found in the lookup table"));
} catch(e) {
alert(e); // shows the error
}
If nothing catches it there either, it keeps bubbling outward through the call stack as usual, and an uncaught error will halt the script.
generator.return
generator.return(value) forces the generator to finish immediately and hands back the value you supply:
function* gen() {
yield 1;
yield 2;
yield 3;
}
const g = gen();
g.next(); // { value: 1, done: false }
g.return('stop'); // { value: "stop", done: true }
g.next(); // { value: undefined, done: true }
After return(), the generator is finished. Later next() calls give the usual idle result, {value: undefined, done: true}.
You won’t reach for return() often, since usually you want all of a generator’s values. It earns its keep when you need to stop a generator at a chosen point and give a definitive final result.
Summary
- Generator functions are written
function* f(…) {…}. - The
yieldoperator lives inside generators, and nowhere else. - Calling
next()runs the body to the nextyieldand returns{value, done}; a finished generator returns{value: undefined, done: true}. yieldis bidirectional:next(value)feeds a value back in as the result of the pausedyield, andthrow(err)raises an error there instead.yield*delegates to another generator, streaming its values through without buffering.generator.return(value)ends a generator early and yields a final result.
Generators aren’t something you write every day. But the ability of a function to swap data with its caller mid-execution is genuinely unusual, and it’s the cleanest tool around for defining custom iterables.
Up next: async generators, which extend all of this to data that arrives asynchronously — think paginated network fetches read with for await ... of. Streamed data is everywhere in web programming, so that use case matters a lot.