Iterators & Generators
Build custom iteration protocols using Symbol.iterator and generator functions to produce lazy, on-demand sequences.
Overview
Key Points
- •The iterable protocol requires a Symbol.iterator method that returns an object with a next() method
- •next() must return { value: any, done: boolean } when done is true the iteration is complete
- •Generator functions use function* syntax; yield pauses and returns a value to the caller
- •A generator object is both an iterator and an iterable, making it usable in for-of loops directly
- •Generators are lazy: no code runs until the first next() call, enabling infinite sequences
- •return inside a generator sets done: true; throw() propagates an error into the generator body
- •next(value) passes a value back into the generator the yield expression evaluates to that value
- •Generator delegation with yield* forwards iteration to another iterable, including other generators
Examples
Custom iterable class and manual iterator protocol
class Range {
constructor(start, end, step = 1) {
this.start = start;
this.end = end;
this.step = step;
}
[Symbol.iterator]() {
let current = this.start;
const { end, step } = this;
return {
next() {
if (current <= end) {
const value = current;
current += step;
return { value, done: false };
}
return { value: undefined, done: true };
}
};
}
}
const range = new Range(1, 10, 2);
console.log([...range]); // [1, 3, 5, 7, 9]
for (const n of new Range(0, 4)) {
process.stdout.write(n + " "); // 0 1 2 3 4
}Because Range implements Symbol.iterator, spread, for-of, and destructuring all work on it automatically.
Generator function infinite Fibonacci sequence
function* fibonacci() {
let [a, b] = [0, 1];
while (true) {
yield a;
[a, b] = [b, a + b];
}
}
function take(n, iterable) {
const result = [];
for (const value of iterable) {
result.push(value);
if (result.length === n) break;
}
return result;
}
console.log(take(8, fibonacci()));
// [0, 1, 1, 2, 3, 5, 8, 13]
// Manual control
const gen = fibonacci();
console.log(gen.next().value); // 0
console.log(gen.next().value); // 1
console.log(gen.next().value); // 1The generator runs forever but the for-of loop breaks early no infinite loop, because generators are lazy.
Two-way communication with next(value) and generator delegation
function* calculator() {
let result = 0;
while (true) {
const input = yield result;
if (input === null) break;
result += input;
}
return result;
}
const calc = calculator();
calc.next(); // start the generator (result = 0)
console.log(calc.next(10).value); // 10
console.log(calc.next(5).value); // 15
console.log(calc.next(3).value); // 18
console.log(calc.next(null)); // { value: 18, done: true }
// Generator delegation with yield*
function* flatten(arr) {
for (const item of arr) {
if (Array.isArray(item)) yield* flatten(item);
else yield item;
}
}
console.log([...flatten([1, [2, [3, [4]], 5]])]);
// [1, 2, 3, 4, 5]The first next() call primes the generator to the first yield. Subsequent calls pass values in and receive the next yielded value.
Common Mistakes
- Calling a generator function without calling next() and wondering why nothing executes the body only runs when driven by next()
- Reusing an exhausted generator once done: true is returned, the generator is permanently finished and returns { value: undefined, done: true } forever
- Forgetting that the first next(value) call cannot pass a value in the argument is discarded because there is no yield expression waiting to receive it yet
- Confusing yield (pause and emit) with return (end the generator) only return sets done: true
Note
Use generators to model pipelines: compose map, filter, and take as generators that accept iterables, then chain them together. Because each stage is lazy, you process only as many items as needed without building intermediate arrays.
Exercises
Write JavaScript code to solve each exercise. Run it and check your output.
Completed: 0 / 5 exercises
Knowledge Check
Question 1 of 4