JavaScript Generators: Mastering Iterative Control Flow
Learn JavaScript generators for powerful iteration control. Master generator functions, yield expressions, and async generators for complex control flow patterns.
Generators are special functions that can pause execution and resume later, allowing you to write iterative algorithms in a more elegant way. They provide powerful control over iteration and asynchronous flow.
Understanding Generators
Basic Generator Syntax
// Generator function declaration
function* simpleGenerator() {
yield 1;
yield 2;
yield 3;
}
// Generator function expression
const genFunc = function* () {
yield 'a';
yield 'b';
};
// Using generators
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 }
// Generators are iterable
for (const value of simpleGenerator()) {
console.log(value); // 1, 2, 3
}
Generator Object Protocol
function* generatorProtocol() {
console.log('Generator started');
const value1 = yield 'First yield';
console.log('Received:', value1);
const value2 = yield 'Second yield';
console.log('Received:', value2);
return 'Generator done';
}
const gen = generatorProtocol();
console.log(gen.next()); // Generator started
// { value: 'First yield', done: false }
console.log(gen.next('Hello')); // Received: Hello
// { value: 'Second yield', done: false }
console.log(gen.next('World')); // Received: World
// { value: 'Generator done', done: true }
Yield Expressions
Basic Yield
function* yieldExamples() {
// Simple yield
yield 42;
// Yield with expression
yield 2 + 2;
// Yield undefined
yield;
// Multiple yields in one line
(yield 1, yield 2, yield 3); // Only the last yield is effective
}
const gen = yieldExamples();
console.log([...gen]); // [42, 4, undefined, 3]
Yield Delegation (yield*)
function* innerGenerator() {
yield 'inner 1';
yield 'inner 2';
}
function* outerGenerator() {
yield 'outer 1';
yield* innerGenerator(); // Delegate to another generator
yield 'outer 2';
yield* [1, 2, 3]; // Delegate to any iterable
}
const gen = outerGenerator();
console.log([...gen]);
// ['outer 1', 'inner 1', 'inner 2', 'outer 2', 1, 2, 3]
// Yield delegation with return values
function* genA() {
yield 1;
yield 2;
return 'A done';
}
function* genB() {
yield 'B start';
const result = yield* genA();
yield `Got result: ${result}`;
}
for (const value of genB()) {
console.log(value);
}
// B start
// 1
// 2
// Got result: A done
Practical Generator Patterns
1. Infinite Sequences
// Fibonacci generator
function* fibonacci() {
let [prev, curr] = [0, 1];
while (true) {
yield curr;
[prev, curr] = [curr, prev + curr];
}
}
// Take first n values
function* take(n, iterable) {
let count = 0;
for (const item of iterable) {
if (count >= n) return;
yield item;
count++;
}
}
const first10Fib = [...take(10, fibonacci())];
console.log(first10Fib);
// [1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
// ID generator
function* idGenerator(prefix = 'ID') {
let id = 1;
while (true) {
yield `${prefix}-${id++}`;
}
}
const userIds = idGenerator('USER');
console.log(userIds.next().value); // USER-1
console.log(userIds.next().value); // USER-2
2. Data Processing Pipelines
// Generator pipeline functions
function* map(fn, iterable) {
for (const item of iterable) {
yield fn(item);
}
}
function* filter(predicate, iterable) {
for (const item of iterable) {
if (predicate(item)) {
yield item;
}
}
}
function* flatMap(fn, iterable) {
for (const item of iterable) {
yield* fn(item);
}
}
// Usage
const numbers = [1, 2, 3, 4, 5];
const pipeline = map(
(x) => x ** 2,
filter((x) => x % 2 === 0, numbers)
);
console.log([...pipeline]); // [4, 16]
// Complex pipeline
function* complexPipeline(data) {
yield* map(
(x) => x * 2,
filter(
(x) => x > 2,
flatMap((x) => [x, x + 0.5], data)
)
);
}
console.log([...complexPipeline([1, 2, 3, 4])]);
// [6, 7, 8, 9]
3. Tree Traversal
// Tree structure
const tree = {
value: 1,
children: [
{
value: 2,
children: [
{ value: 4, children: [] },
{ value: 5, children: [] },
],
},
{
value: 3,
children: [{ value: 6, children: [] }],
},
],
};
// Depth-first traversal
function* depthFirst(node) {
yield node.value;
for (const child of node.children) {
yield* depthFirst(child);
}
}
console.log([...depthFirst(tree)]); // [1, 2, 4, 5, 3, 6]
// Breadth-first traversal
function* breadthFirst(node) {
const queue = [node];
while (queue.length > 0) {
const current = queue.shift();
yield current.value;
queue.push(...current.children);
}
}
console.log([...breadthFirst(tree)]); // [1, 2, 3, 4, 5, 6]
4. State Machines
function* trafficLight() {
while (true) {
// Green
const greenTime = yield { color: 'green', duration: 30 };
// Yellow
const yellowTime = yield { color: 'yellow', duration: 5 };
// Red
const redTime = yield { color: 'red', duration: 25 };
}
}
const light = trafficLight();
// Simulate traffic light
function runTrafficLight() {
let current = light.next();
const interval = setInterval(() => {
console.log(`Light: ${current.value.color}`);
current = light.next();
}, 1000);
// Stop after 10 seconds
setTimeout(() => clearInterval(interval), 10000);
}
Error Handling in Generators
function* errorHandlingGenerator() {
try {
yield 1;
yield 2;
yield 3;
} catch (error) {
console.log('Caught error:', error.message);
yield 'Error handled';
} finally {
console.log('Cleanup');
yield 'Finally';
}
}
const gen = errorHandlingGenerator();
console.log(gen.next()); // { value: 1, done: false }
console.log(gen.next()); // { value: 2, done: false }
// Throw error into generator
console.log(gen.throw(new Error('Something went wrong')));
// Caught error: Something went wrong
// { value: 'Error handled', done: false }
console.log(gen.next()); // Cleanup
// { value: 'Finally', done: false }
// Generator return method
function* returnExample() {
try {
yield 1;
yield 2;
yield 3;
} finally {
console.log('Generator terminated');
}
}
const gen2 = returnExample();
console.log(gen2.next()); // { value: 1, done: false }
console.log(gen2.return('Stopped')); // Generator terminated
// { value: 'Stopped', done: true }
Async Generators
Basic Async Generators
// Async generator function
async function* asyncGenerator() {
yield await Promise.resolve(1);
yield await Promise.resolve(2);
yield await Promise.resolve(3);
}
// Consuming async generators
(async () => {
for await (const value of asyncGenerator()) {
console.log(value); // 1, 2, 3
}
})();
// Async generator with delays
async function* delayedCounter() {
let count = 0;
while (count < 5) {
await new Promise((resolve) => setTimeout(resolve, 1000));
yield ++count;
}
}
// Stream-like processing
async function* processStream(stream) {
for await (const chunk of stream) {
// Process chunk
const processed = chunk.toUpperCase();
yield processed;
}
}
Real-World Async Generator Examples
// Paginated API requests
async function* paginatedFetch(baseUrl) {
let page = 1;
let hasMore = true;
while (hasMore) {
const response = await fetch(`${baseUrl}?page=${page}`);
const data = await response.json();
yield* data.items;
hasMore = data.hasNextPage;
page++;
}
}
// Usage
(async () => {
const apiData = paginatedFetch('https://api.example.com/users');
for await (const user of apiData) {
console.log(user);
// Process each user as they arrive
}
})();
// File line reader
async function* readLines(file) {
const text = await file.text();
const lines = text.split('\n');
for (const line of lines) {
// Simulate async processing
await new Promise((resolve) => setTimeout(resolve, 10));
yield line.trim();
}
}
// Rate-limited async generator
async function* rateLimited(items, delay) {
for (const item of items) {
yield item;
await new Promise((resolve) => setTimeout(resolve, delay));
}
}
Advanced Patterns
Generator Composition
// Compose multiple generators
function* compose(...generators) {
for (const gen of generators) {
yield* gen();
}
}
function* gen1() {
yield 1;
yield 2;
}
function* gen2() {
yield 3;
yield 4;
}
const composed = compose(gen1, gen2);
console.log([...composed]); // [1, 2, 3, 4]
// Zip generators
function* zip(...iterables) {
const iterators = iterables.map((it) => it[Symbol.iterator]());
while (true) {
const results = iterators.map((it) => it.next());
if (results.some((r) => r.done)) {
return;
}
yield results.map((r) => r.value);
}
}
const zipped = zip([1, 2, 3], ['a', 'b', 'c'], [true, false, true]);
console.log([...zipped]); // [[1, 'a', true], [2, 'b', false], [3, 'c', true]]
Coroutines with Generators
// Simple coroutine scheduler
function* coroutine1() {
console.log('Coroutine 1: Start');
yield;
console.log('Coroutine 1: Middle');
yield;
console.log('Coroutine 1: End');
}
function* coroutine2() {
console.log('Coroutine 2: Start');
yield;
console.log('Coroutine 2: Middle');
yield;
console.log('Coroutine 2: End');
}
function scheduler(...coroutines) {
const tasks = coroutines.map((co) => co());
while (tasks.length > 0) {
const task = tasks.shift();
const { done } = task.next();
if (!done) {
tasks.push(task);
}
}
}
scheduler(coroutine1, coroutine2);
// Interleaved execution of coroutines
Generator-based State Management
function* createStore(initialState, reducer) {
let state = initialState;
while (true) {
const action = yield state;
if (action) {
state = reducer(state, action);
}
}
}
// Usage
const reducer = (state, action) => {
switch (action.type) {
case 'INCREMENT':
return { ...state, count: state.count + 1 };
case 'DECREMENT':
return { ...state, count: state.count - 1 };
default:
return state;
}
};
const store = createStore({ count: 0 }, reducer);
console.log(store.next().value); // { count: 0 }
console.log(store.next({ type: 'INCREMENT' }).value); // { count: 1 }
console.log(store.next({ type: 'INCREMENT' }).value); // { count: 2 }
console.log(store.next({ type: 'DECREMENT' }).value); // { count: 1 }
Performance Considerations
// Lazy evaluation with generators
function* range(start, end, step = 1) {
for (let i = start; i < end; i += step) {
yield i;
}
}
// Memory efficient - values generated on demand
const bigRange = range(0, 1000000);
// Only generates what's needed
function* take(n, iterable) {
let count = 0;
for (const item of iterable) {
if (count++ >= n) return;
yield item;
}
}
const first5 = [...take(5, bigRange)];
console.log(first5); // [0, 1, 2, 3, 4]
Best Practices
- Use generators for lazy evaluation when dealing with large datasets
- Prefer async generators for asynchronous iteration
- Document generator behavior especially for complex flows
- Handle cleanup in finally blocks
- Consider memory usage when yielding large objects
- Use yield* for delegation to keep code clean
Conclusion
Generators provide a powerful way to control iteration and manage complex control flows in JavaScript. They enable lazy evaluation, simplify asynchronous code, and offer elegant solutions for problems involving sequences and state management. While they have a learning curve, mastering generators opens up new possibilities for writing efficient and maintainable code.