JavaScript Generators: function* and yield
Generators are special functions that can pause their execution and resume later, maintaining their internal state between pauses. Unlike regular functions that run to completion and return a single...
Key Insights
- Generators are pausable functions that produce sequences of values on demand, enabling lazy evaluation and memory-efficient iteration over potentially infinite datasets
- The
yieldkeyword creates two-way communication channels—generators can pause to send values out and resume with values passed back in via.next(value) - While async/await has replaced generators for most asynchronous code, generators remain the best tool for custom iterators, infinite sequences, and stateful iteration patterns
What Are Generators?
Generators are special functions that can pause their execution and resume later, maintaining their internal state between pauses. Unlike regular functions that run to completion and return a single value, generators can yield multiple values over time, one at a time.
The syntax uses function* (note the asterisk) to declare a generator, and the yield keyword to pause execution and return a value:
function* simpleGenerator() {
yield 1;
yield 2;
yield 3;
}
const gen = simpleGenerator();
console.log(gen.next()); // { value: 1, done: false }
console.log(gen.next()); // { value: 2, done: false }
console.log(gen.next()); // { value: 3, done: false }
console.log(gen.next()); // { value: undefined, done: true }
Each call to next() runs the generator until it hits a yield, returns that value, and pauses. The generator remembers where it stopped, including all local variables and execution context.
Generator Basics: Creating and Consuming
Generator functions implement the iterator protocol automatically. When you call a generator function, it doesn’t execute the function body—instead, it returns a generator object that you control by calling .next().
The .next() method returns an object with two properties: value (the yielded value) and done (a boolean indicating whether the generator has finished).
function* idGenerator() {
let id = 1;
while (true) {
yield id++;
}
}
const ids = idGenerator();
console.log(ids.next().value); // 1
console.log(ids.next().value); // 2
console.log(ids.next().value); // 3
// Each generator instance maintains its own state
const moreIds = idGenerator();
console.log(moreIds.next().value); // 1
console.log(moreIds.next().value); // 2
This example demonstrates an infinite generator—it never sets done to true. This is perfectly valid and useful for cases where you need an unbounded sequence of values.
yield Expressions and Two-Way Communication
Here’s where generators get interesting: yield isn’t just a one-way street. You can pass values back into a generator through .next(value), and the generator receives that value as the result of the yield expression.
function* calculator() {
let result = 0;
while (true) {
const operation = yield result;
if (!operation) continue;
const [operator, operand] = operation;
switch (operator) {
case 'add':
result += operand;
break;
case 'multiply':
result *= operand;
break;
case 'reset':
result = 0;
break;
}
}
}
const calc = calculator();
console.log(calc.next()); // { value: 0, done: false }
console.log(calc.next(['add', 5])); // { value: 5, done: false }
console.log(calc.next(['multiply', 3])); // { value: 15, done: false }
console.log(calc.next(['add', 10])); // { value: 25, done: false }
console.log(calc.next(['reset'])); // { value: 0, done: false }
The first next() call starts the generator and runs until the first yield. Subsequent calls pass values that become the result of the yield expression, allowing external code to influence the generator’s behavior.
Practical Use Cases
Generators excel at producing sequences lazily—values are computed only when requested. This makes them perfect for infinite sequences or expensive computations.
function* fibonacci() {
let [prev, curr] = [0, 1];
while (true) {
yield curr;
[prev, curr] = [curr, prev + curr];
}
}
// Take only what you need
const fib = fibonacci();
for (let i = 0; i < 10; i++) {
console.log(fib.next().value);
}
// Or use with for...of (which automatically calls .next())
function* range(start, end, step = 1) {
for (let i = start; i < end; i += step) {
yield i;
}
}
for (const num of range(0, 10, 2)) {
console.log(num); // 0, 2, 4, 6, 8
}
The for...of loop automatically consumes generators, calling .next() until done is true. This makes generators a natural fit for custom iteration logic.
Advanced Patterns
The yield* expression delegates to another generator or iterable, yielding all its values before continuing:
function* flatten(arr) {
for (const item of arr) {
if (Array.isArray(item)) {
yield* flatten(item);
} else {
yield item;
}
}
}
const nested = [1, [2, 3, [4, 5]], 6, [7]];
console.log([...flatten(nested)]); // [1, 2, 3, 4, 5, 6, 7]
Generators also support .throw() for error handling and .return() for early termination:
function* errorHandling() {
try {
yield 1;
yield 2;
yield 3;
} catch (e) {
console.log('Caught:', e.message);
yield 'recovered';
}
yield 'continued';
}
const gen = errorHandling();
console.log(gen.next()); // { value: 1, done: false }
console.log(gen.throw(new Error('Oops'))); // Logs "Caught: Oops"
// { value: 'recovered', done: false }
console.log(gen.next()); // { value: 'continued', done: false }
The .return(value) method terminates the generator immediately and returns the specified value:
const gen2 = errorHandling();
console.log(gen2.next()); // { value: 1, done: false }
console.log(gen2.return('done')); // { value: 'done', done: true }
console.log(gen2.next()); // { value: undefined, done: true }
Real-World Applications
Generators shine in state machine implementations where you need to maintain state across multiple steps:
function* trafficLight() {
while (true) {
yield 'green';
yield 'yellow';
yield 'red';
}
}
const light = trafficLight();
setInterval(() => {
const state = light.next().value;
console.log(`Light is now: ${state}`);
// Update UI accordingly
}, 2000);
Before async/await, generators powered asynchronous control flow libraries. While largely superseded, understanding this pattern helps grasp how async/await works under the hood:
function* fetchUserData() {
const user = yield fetch('/api/user');
const posts = yield fetch(`/api/users/${user.id}/posts`);
return { user, posts };
}
// A runner would handle the promises (simplified)
function run(generator) {
const gen = generator();
function handle(result) {
if (result.done) return Promise.resolve(result.value);
return Promise.resolve(result.value)
.then(res => handle(gen.next(res)));
}
return handle(gen.next());
}
Generators also excel at data pipeline processing where you transform data step-by-step:
function* map(iterable, fn) {
for (const item of iterable) {
yield fn(item);
}
}
function* filter(iterable, predicate) {
for (const item of iterable) {
if (predicate(item)) yield item;
}
}
function* take(iterable, n) {
let count = 0;
for (const item of iterable) {
if (count++ >= n) break;
yield item;
}
}
// Compose a pipeline
const numbers = range(1, 100);
const doubled = map(numbers, x => x * 2);
const evens = filter(doubled, x => x % 4 === 0);
const firstTen = take(evens, 10);
console.log([...firstTen]); // [4, 8, 12, 16, 20, 24, 28, 32, 36, 40]
Performance and Best Practices
Use generators when you need:
- Lazy evaluation: Computing values on demand rather than upfront
- Infinite sequences: ID generators, pagination, streaming data
- Custom iteration: Complex traversal logic for trees, graphs, or custom data structures
- Memory efficiency: Processing large datasets without loading everything into memory
Avoid generators when:
- You need all values immediately (use regular arrays)
- Performance is critical in tight loops (generator overhead adds up)
- The logic is simple enough for array methods like
mapandfilter
Important gotchas:
- Generators are one-time use. Once exhausted, you need to create a new instance.
- The first
.next()call starts execution but doesn’t receive the passed value—that value is discarded. - Returning a value from a generator sets
done: true, butfor...ofdoesn’t yield that final value.
For asynchronous iteration, use async generators:
async function* fetchPages(url) {
let page = 1;
while (true) {
const response = await fetch(`${url}?page=${page++}`);
const data = await response.json();
if (data.items.length === 0) break;
yield data.items;
}
}
for await (const items of fetchPages('/api/items')) {
console.log(items);
}
Generators remain a powerful tool in modern JavaScript. While async/await handles most asynchronous patterns more elegantly, generators are still the best choice for custom iterators, stateful sequences, and lazy evaluation scenarios. Master them, and you’ll have a versatile tool for solving complex iteration problems efficiently.