Generators
What is a Generator?
A generator function returns a generator object — an iterator that can pause and resume execution. The yield keyword pauses the function and returns a value.
javascriptfunction* counter() { // function* syntax
console.log('start');
yield 1; // pause, return { value: 1, done: false }
console.log('middle');
yield 2; // pause, return { value: 2, done: false }
console.log('end');
return 3; // { value: 3, done: true } — function complete
}
const gen = counter(); // creates generator object (doesn't run yet!)
gen.next(); // 'start' → { value: 1, done: false }
gen.next(); // 'middle' → { value: 2, done: false }
gen.next(); // 'end' → { value: 3, done: true }
gen.next(); // { value: undefined, done: true } — already doneThe generator is lazy — code runs only when .next() is called.
yield* — Delegating to Another Iterable
yield* delegates iteration to any iterable — another generator, an array, a string, or any [Symbol.iterator]-compliant object — yielding each of its values in sequence as if they were produced inline. This solves the composition problem: without yield*, you'd need a manual for...of loop inside the generator to flatten a nested source. It also propagates the final return value of a delegated generator back as the expression value of the yield* statement itself, enabling coroutine-style pipelines where subgenerators can pass results back up to callers.
javascriptfunction* inner() {
yield 'a';
yield 'b';
}
function* outer() {
yield 1;
yield* inner(); // delegate to inner — 'a', 'b' are yielded
yield* [3, 4]; // yield* works with any iterable
yield 5;
}
[...outer()]; // [1, 'a', 'b', 3, 4, 5]Generators are Iterators AND Iterables
A generator object is self-referential: its [Symbol.iterator]() method returns this, making it both an iterator (has next()) and an iterable (has [Symbol.iterator]()). This dual nature means you can use a generator object directly with for...of, spread, or destructuring without needing to call [Symbol.iterator]() first. The practical consequence is that a generator returned from a function is immediately usable everywhere an iterable is expected — you don't need a wrapper object.
javascriptfunction* range(start, end, step = 1) {
for (let i = start; i <= end; i += step) {
yield i;
}
}
const r = range(1, 10, 2);
// As iterator:
r.next(); // { value: 1, done: false }
r.next(); // { value: 3, done: false }
// As iterable (has [Symbol.iterator] that returns itself):
for (const n of range(1, 5)) console.log(n); // 1 2 3 4 5
[...range(1, 5)]; // [1, 2, 3, 4, 5]Infinite Sequences
Generators are perfect for infinite sequences:
javascriptfunction* fibonacci() {
let [a, b] = [0, 1];
while (true) { // infinite!
yield a;
[a, b] = [b, a + b];
}
}
function* naturals() {
let n = 1;
while (true) yield n++;
}
// Take first N from infinite sequence:
function take(n, iter) {
const result = [];
for (const val of iter) {
result.push(val);
if (result.length >= n) break;
}
return result;
}
take(8, fibonacci()); // [0, 1, 1, 2, 3, 5, 8, 13]
take(5, naturals()); // [1, 2, 3, 4, 5]Two-Way Communication: next(value)
You can pass values INTO a generator via next(value):
javascriptfunction* calculator() {
let total = 0;
while (true) {
const input = yield total; // pauses AND receives the next value
if (input === null) break;
total += input;
}
return total;
}
const calc = calculator();
calc.next(); // { value: 0, done: false } — initialize (first next has no effect on yield)
calc.next(10); // { value: 10, done: false } — added 10
calc.next(20); // { value: 30, done: false } — added 20
calc.next(5); // { value: 35, done: false } — added 5
calc.next(null); // { value: 35, done: true } — doneNote: The first next() call cannot pass a value (there's no yield waiting to receive it yet).
return() and throw()
return() and throw() are the two external control methods on a generator object. return(value) forces the generator to finalize immediately — it triggers any finally blocks and returns { value, done: true }. throw(error) injects an exception at the generator's current suspension point, as if the yield expression had thrown; if the generator has a try/catch wrapping the yield, it can intercept the error and continue. These methods are how the runtime communicates early termination and error conditions back into suspended generator code, and why generators are the correct primitive for resource-managing iterators.
javascriptfunction* gen() {
try {
yield 1;
yield 2;
yield 3;
} finally {
console.log('cleanup'); // always runs
}
}
const g = gen();
g.next(); // { value: 1, done: false }
// Forcefully terminate:
g.return(99); // logs 'cleanup' → { value: 99, done: true }
g.next(); // { value: undefined, done: true }
// Throw an error into the generator:
const g2 = gen();
g2.next(); // { value: 1, done: false }
g2.throw(new Error('oops')); // logs 'cleanup', throws if not caught insidePractical Use Cases
Lazy Data Transformation Pipeline
A generator pipeline processes data one element at a time through a chain of transformations, allocating no intermediate arrays. Each stage only requests the next value from its source when the downstream consumer requests a value from it — this is pull-based, or "lazy," evaluation. This is valuable when working with large datasets (files, database cursors, infinite sequences) where materializing all intermediate results would be prohibitively expensive. The entire pipeline shares a single pass over the data regardless of how many transformation stages are stacked.
javascriptfunction* map(iter, fn) {
for (const val of iter) yield fn(val);
}
function* filter(iter, pred) {
for (const val of iter) {
if (pred(val)) yield val;
}
}
function* take(n, iter) {
let count = 0;
for (const val of iter) {
yield val;
if (++count >= n) return;
}
}
// Lazy pipeline — only computes what's needed:
const result = take(3,
filter(
map(naturals(), x => x * x), // square of naturals: 1,4,9,16,...
x => x % 2 === 0 // even squares: 4,16,36,...
)
);
[...result]; // [4, 16, 36] — only computed 6 naturals!Unique ID Generator
Generators are a clean fit for any stateful counter or sequence that must produce unique values across multiple call sites. Because the generator's local state (the id variable) is private and persistent between yield calls, there is no global mutable variable to accidentally reset or share incorrectly. Multiple independent generator instances share no state, so different ID namespaces remain isolated without any coordination.
javascriptfunction* idGenerator(prefix = '') {
let id = 1;
while (true) {
yield `${prefix}${id++}`;
}
}
const userIds = idGenerator('USR-');
const orderIds = idGenerator('ORD-');
userIds.next().value; // 'USR-1'
userIds.next().value; // 'USR-2'
orderIds.next().value; // 'ORD-1'
userIds.next().value; // 'USR-3'State Machine
Generators naturally encode state machines because yield checkpoints the exact position in the function — the program counter is part of the generator's saved state. Each next() call advances to the next valid state without any explicit switch/case or state variable tracking. The generator body reads like a sequential description of the state sequence, which is far easier to reason about and modify than a traditional state-variable approach.
javascriptfunction* trafficLight() {
while (true) {
yield 'green';
yield 'yellow';
yield 'red';
}
}
const light = trafficLight();
light.next().value; // 'green'
light.next().value; // 'yellow'
light.next().value; // 'red'
light.next().value; // 'green' (repeats)Generator vs Regular Function
| Regular Function | Generator | |
|---|---|---|
| Return type | Single value | Multiple values (lazy) |
| Execution | Run to completion | Pauseable |
| State | No internal state between calls | Maintains state between yield |
| Used for | Computation | Sequences, state machines, co-routines |
Interview Questions
Q: What does yield do?
A: yield pauses the generator function and returns a value to the caller as { value, done: false }. Execution resumes on the next next() call, optionally receiving a value passed to next(value).
Q: What is the difference between return and yield in a generator?
A: yield pauses and produces a value while done remains false. return ends the generator and produces a final value with done: true. After return, further next() calls return { value: undefined, done: true }.
Q: Why are generators useful for infinite sequences?
A: Generators are lazy — they compute values on demand. An infinite while(true) loop with yield only runs as far as the consumer requests. No memory is wasted pre-computing all values.