JAVASCRIPT / DESTRUCTURING, ITERABLES, AND GENERATORS
Generator functions and lazy yield
Write function* generators whose bodies stay paused between next() calls, so each value is computed only at the moment a consumer asks for it.
What you will learn
- Declare function* and read the { value, done } envelope next() returns
- Trace when a generator body actually runs by counting next() calls
- Keep loop counters and locals alive across yields with no extra state object
- Pull three values out of an unbounded sequence and leave the rest uncomputed
Understanding Generator functions and lazy yield
The star in function* name() changes what calling the function does. A normal call pushes a frame, runs the body to completion, and pops it; a generator call builds the frame, parks it before the first statement, and returns an object that owns that parked frame. That is why a console.log on the very first line of a generator prints nothing when you call it — you have created a suspended computation, not performed one.
yield is a suspend, not a return. When it executes, the frame stops in place and stays alive: local variables, the position in the code, and any open loops are all preserved, so the following next() resumes at the statement after that yield rather than restarting the body. Each next() hands back an envelope { value, done }, and done only becomes true when the body hits a return or runs off the end. A while (true) wrapped around a yield therefore does not hang; it advances exactly one step per pull.
Laziness falls out of this design. The producer cannot run ahead of the consumer, so a generator that describes a million values does a million values' worth of work only if something pulls a million times. The cost is that the work now happens on the consumer's clock: an argument check or a thrown error at the top of the body surfaces at the first next() call, possibly in code far away from where the generator was created.
function* steps() {
console.log('body: started');
yield 'a';
console.log('body: resumed after first yield');
yield 'b';
console.log('body: resumed after second yield');
return 'end';
}
const it = steps();
console.log('generator object created, body not started');
for (let i = 0; i < 4; i++) {
const r = it.next();
console.log(`next -> value=${r.value} done=${r.done}`);
}Calling a generator function produces a paused frame, and each next() runs it only as far as the next yield, so values are computed on demand.
Worked examples
Only the pulled work runs
A generator written for a million values does two iterations because only two values were requested.
let computed = 0;
function* squares(limit) {
for (let n = 1; n <= limit; n++) {
computed++;
yield n * n;
}
}
const it = squares(1000000);
console.log(it.next().value);
console.log(it.next().value);
console.log('loop iterations actually executed:', computed);Example explained
Line 1squares(1000000) runs no loop iterations at all; it only creates the generator object.
Line 2The first next() enters the for loop, increments computed to 1, and stops at yield with n still equal to 1.
Line 3The second next() resumes inside the loop, runs n++ and the next check, and yields 4.
Line 4computed proves the limit of 1000000 is a description of possible work, not work performed.
Each generator object has its own frame
Two generator objects from the same function keep separate copies of the local variable n across suspensions.
function* ticket() {
let n = 1;
while (n <= 3) {
yield 'T' + n;
n++;
}
}
const a = ticket();
const b = ticket();
console.log(a.next().value);
console.log(a.next().value);
console.log(b.next().value);
console.log(a.next().value);
console.log(a.next().done);
console.log(b.next().value);Example explained
Line 1let n = 1 runs once per generator object, on that object's first next() call.
Line 2b.next() returns T1 while a is already at T2, because a and b own independent parked frames.
Line 3n++ runs when the frame is resumed, not when the value is yielded, so n survives the pause without being stored anywhere else.
Line 4The fifth call finds n === 4, the while condition fails, the body ends, and done becomes true.
Destructuring pulls a fixed number of values
An endless generator can be safely destructured because the pattern decides how many times next() is called.
function* naturals() {
let n = 0;
while (true) {
console.log('producing', n);
yield n++;
}
}
const [a, b, c] = naturals();
console.log('got', a, b, c);Example explained
Line 1The three binding targets a, b, c cause exactly three next() calls on the generator.
Line 2'producing 2' is the last producer log: destructuring then closes the generator instead of pulling a fourth value.
Line 3while (true) never spins forever because the frame is parked at yield between pulls.
Line 4yield n++ hands out the old value of n and leaves the increment to the following resume.
Important notes
A generator object is single use: after done: true, every further next() returns { value: undefined, done: true }, and restarting requires calling the generator function again.
yield is only valid directly inside the generator body, so it cannot appear in a nested callback such as items.forEach(x => yield x), and arrow functions can never be generators.
Common mistakes
Putting argument validation at the top of a generator body and expecting it to throw at call time; the error is delayed until someone calls next(), often in unrelated code.
Calling the generator function again inside a loop instead of reusing one generator object, which creates a fresh frame each time and returns the first value forever.
Ending a generator with return value instead of yield value; for...of and destructuring drop the value that arrives with done: true, so it silently disappears.
Try it yourself
Change, predict, then run
Write function* fib() that yields Fibonacci numbers forever and logs each one it is about to yield, then pull exactly six values with six next() calls and confirm the producer logged six times and no more.
Open the JavaScript workspaceCheck your understanding
A generator body begins with console.log('setup'). You call the generator function three times and keep the three returned objects, but never call next(). How many times is 'setup' printed?
- Zero times, because calling a generator function only creates paused generator objects
- Three times, because each call runs the body up to the first yield
- Once, because the three generator objects share a single frame
- Three times, but only after the surrounding script finishes running
Show answer
A generator call builds a frame and parks it before the first statement, so no body code has executed yet and nothing is printed. Option two is tempting because running up to the first yield is exactly what happens — but that is the job of the first next() call, not of the call that creates the generator.