Difficulty: Advanced
Generators are special functions declared with the function* syntax that can pause and resume their execution. When called, a generator function doesn't run its body immediately - instead, it returns a generator object that conforms to both the iterable and iterator protocols. Each call to the generator's next() method resumes execution from where it last paused until it hits the next yield expression or returns.
The yield keyword is what makes generators powerful. When execution reaches a yield expression, the generator pauses and produces a value to the caller. The state of the function - local variables, execution position, and scope - is preserved between yields. This is fundamentally different from regular functions, which must run to completion once called. The ability to pause and resume makes generators ideal for lazy evaluation, producing values on demand rather than computing everything upfront.
Generators support two-way communication through the next() method. When you call generator.next(value), the value you pass becomes the result of the yield expression inside the generator. This means yield is both an output (producing a value) and an input (receiving a value) mechanism. The first next() call starts the generator, and any value passed to the first next() is ignored since there's no yield expression waiting to receive it.
The yield* expression delegates iteration to another iterable or generator. When a generator encounters yield*, it yields each value from the delegated iterable before continuing with its own code. This enables composition - breaking complex generator logic into smaller, reusable generator functions. yield* also forwards next() values and return values, making delegation transparent.
Infinite sequences are one of the most elegant applications of generators. Since generators produce values lazily, you can create a generator that conceptually produces an infinite number of values without consuming infinite memory. Consumers take only what they need. Common examples include ID generators, Fibonacci sequences, and infinite range counters. Combined with destructuring or itertools-like utilities, infinite generators become powerful building blocks.
Historically, generators were used with libraries like co.js to manage asynchronous flow before async/await existed. A generator would yield promises, and a runner function would await each promise and feed the result back via next(). While async/await has replaced this pattern, understanding it provides insight into how async/await works under the hood. Today, generators remain valuable for lazy iteration, state machines, data streaming, and custom control flow.
function* greetings() {
console.log('Generator started');
yield 'Hello';
console.log('Resumed after first yield');
yield 'World';
console.log('Resumed after second yield');
return 'Done!';
}
const gen = greetings();
console.log('Created generator');
console.log(gen.next());
console.log(gen.next());
console.log(gen.next());
console.log(gen.next());
// Generators are iterable
function* colors() {
yield 'red';
yield 'green';
yield 'blue';
}
console.log('\nfor...of:');
for (const color of colors()) {
process.stdout.write(color + ' ');
}
console.log();
console.log('Spread:', [...colors()]);
The generator body doesn't execute until the first next() call. Each yield pauses execution and produces a value. The return value has done: true. Note that for...of and spread don't include the return value (only yielded values).
function* conversation() {
const name = yield 'What is your name?';
const age = yield `Hello ${name}! How old are you?`;
return `${name} is ${age} years old.`;
}
const chat = conversation();
// First next() starts the generator
console.log(chat.next()); // yields the first question
// Pass value back through next()
console.log(chat.next('Alice')); // 'Alice' becomes name
console.log(chat.next(28)); // 28 becomes age
// Practical: accumulator generator
function* runningTotal() {
let total = 0;
while (true) {
const value = yield total;
if (value === null) return total;
total += value;
}
}
const acc = runningTotal();
console.log('\nRunning total:');
console.log(acc.next().value); // 0 (initial)
console.log(acc.next(10).value); // 10
console.log(acc.next(20).value); // 30
console.log(acc.next(5).value); // 35
console.log(acc.next(null)); // done, returns 35
Values passed to next() become the result of the yield expression inside the generator. The first next() starts execution - its argument is ignored. The running total accumulator demonstrates stateful two-way communication.
// yield* delegates to another iterable
function* inner() {
yield 'a';
yield 'b';
return 'inner done'; // return value of yield*
}
function* outer() {
yield 1;
const result = yield* inner(); // delegates to inner
console.log('Inner returned:', result);
yield 2;
}
console.log([...outer()]);
// Infinite Fibonacci generator
function* fibonacci() {
let a = 0, b = 1;
while (true) {
yield a;
[a, b] = [b, a + b];
}
}
// Take first N values from infinite generator
function take(n, iterable) {
const result = [];
for (const value of iterable) {
result.push(value);
if (result.length >= n) break;
}
return result;
}
console.log('\nFirst 10 Fibonacci:', take(10, fibonacci()));
// Infinite ID generator
function* idGenerator(prefix = 'id') {
let counter = 1;
while (true) {
yield `${prefix}_${String(counter).padStart(4, '0')}`;
counter++;
}
}
const ids = idGenerator('user');
console.log(ids.next().value);
console.log(ids.next().value);
console.log(ids.next().value);
yield* delegates to inner generator, yielding each of its values. The return value of the inner generator becomes the result of yield*. Infinite generators like Fibonacci work because values are produced lazily - only computed when requested.
// Paginated data fetcher (simulated)
function* paginate(items, pageSize) {
for (let i = 0; i < items.length; i += pageSize) {
yield {
page: Math.floor(i / pageSize) + 1,
data: items.slice(i, i + pageSize),
hasMore: i + pageSize < items.length
};
}
}
const allItems = Array.from({ length: 12 }, (_, i) => `item_${i + 1}`);
const pages = paginate(allItems, 4);
let page = pages.next();
while (!page.done) {
console.log(`Page ${page.value.page}:`, page.value.data.join(', '));
console.log(` Has more: ${page.value.hasMore}`);
page = pages.next();
}
// State machine generator
function* trafficLight() {
while (true) {
yield 'GREEN';
yield 'YELLOW';
yield 'RED';
}
}
const light = trafficLight();
const sequence = [];
for (let i = 0; i < 7; i++) {
sequence.push(light.next().value);
}
console.log('\nTraffic lights:', sequence.join(' -> '));
Pagination produces data in chunks on demand. The state machine cycles through traffic light states infinitely. Both demonstrate generators' strength: managing sequential state without external variables or complex logic.
function*, yield, generator iterator, two-way communication, yield*, infinite sequences, async generators