JavaScript Generators
Introduction
JavaScript generators are a special type of function introduced in ES6 (ECMAScript 2015) that can be paused and resumed during execution. Unlike regular functions that run to completion in a single execution, generators can yield control back to the caller at specified points, making them perfect for creating iterators, handling asynchronous operations, and managing complex flows of data.
Generators open up new possibilities for controlling execution flow, making code more readable and maintainable, especially when dealing with sequences or streams of data.
Basic Generator Syntax
A generator function is defined using an asterisk (*
) after the function
keyword or before the function name. Inside the function, we use the yield
keyword to pause execution and return a value.
function* simpleGenerator() {
console.log('Start of generator');
yield 1;
console.log('After first yield');
yield 2;
console.log('After second yield');
yield 3;
console.log('End of generator');
}
// Creating a generator object
const generator = simpleGenerator();
console.log(generator.next()); // { value: 1, done: false }
console.log(generator.next()); // { value: 2, done: false }
console.log(generator.next()); // { value: 3, done: false }
console.log(generator.next()); // { value: undefined, done: true }
How It Works
-
When we call
simpleGenerator()
, it doesn't execute the function body immediately. Instead, it returns a generator object. -
The function only starts executing when we call
next()
on the generator object. -
The function runs until it encounters a
yield
statement, at which point it pauses and returns an object with two properties:value
: The yielded valuedone
: A boolean indicating whether the generator is finished
-
Each time we call
next()
, the function resumes execution from where it was paused until it reaches the nextyield
or finishes.
Generator Iteration
Generators are iterable, which means we can use them in for...of
loops or spread them into arrays:
function* countToFive() {
yield 1;
yield 2;
yield 3;
yield 4;
yield 5;
}
// Using for...of loop
console.log("Iterating with for...of:");
for (const num of countToFive()) {
console.log(num);
}
// Output:
// 1
// 2
// 3
// 4
// 5
// Converting to array
const numbersArray = [...countToFive()];
console.log("Array from generator:", numbersArray);
// Output: Array from generator: [1, 2, 3, 4, 5]
Passing Values to Generators
Generators can receive values from the caller using the next()
method. The value passed to next()
becomes the result of the previous yield
expression:
function* conversation() {
const name = yield "What's your name?";
const hobby = yield `Hello, ${name}! What's your favorite hobby?`;
yield `${name} likes ${hobby}. That's cool!`;
}
const talk = conversation();
console.log(talk.next().value); // What's your name?
console.log(talk.next('Alice').value); // Hello, Alice! What's your favorite hobby?
console.log(talk.next('coding').value); // Alice likes coding. That's cool!
Notice that the first next()
call doesn't accept a value because there's no yield
expression waiting for a value yet.
Infinite Sequences with Generators
Generators are perfect for creating potentially infinite sequences where values are calculated on demand:
function* fibonacciSequence() {
let a = 0, b = 1;
while (true) {
yield a;
[a, b] = [b, a + b];
}
}
const fibonacci = fibonacciSequence();
// Get the first 10 Fibonacci numbers
for (let i = 0; i < 10; i++) {
console.log(fibonacci.next().value);
}
// Output: 0, 1, 1, 2, 3, 5, 8, 13, 21, 34
This generator can theoretically produce an infinite Fibonacci sequence, but we only take what we need.
Error Handling in Generators
Generators have special error handling capabilities. You can throw errors into the generator using the throw()
method:
function* errorHandlingExample() {
try {
yield 'Start';
yield 'Middle';
yield 'End';
} catch (error) {
console.error('Caught an error:', error);
yield 'Error recovery';
}
}
const gen = errorHandlingExample();
console.log(gen.next().value); // Start
console.log(gen.throw('Something went wrong').value); // Caught an error: Something went wrong
// Error recovery
console.log(gen.next().value); // undefined (generator is done)
Generator Delegation with yield*
Generators can delegate to other generators using the yield*
expression:
function* generateNumbers() {
yield 1;
yield 2;
}
function* generateLetters() {
yield 'a';
yield 'b';
}
function* combined() {
yield* generateNumbers();
yield* generateLetters();
yield 'Done!';
}
const iterator = combined();
console.log([...iterator]); // [1, 2, 'a', 'b', 'Done!']
This creates compositional capabilities for generators, where complex iterations can be built from simpler ones.
Practical Examples
Example 1: Paginated API Requests
Generators can help manage paginated API requests in a clean way:
function* fetchPaginatedData(endpoint, pageSize = 10) {
let page = 1;
let hasMore = true;
while (hasMore) {
// This would be an actual API call in a real application
console.log(`Fetching ${endpoint}?page=${page}&pageSize=${pageSize}`);
// Simulate API response
const data = {
items: Array(page < 3 ? pageSize : 5).fill(`Item from page ${page}`),
hasMore: page < 3
};
yield data.items;
hasMore = data.hasMore;
page++;
}
}
async function processAllData() {
const dataGenerator = fetchPaginatedData('/api/products', 10);
for (const pageData of dataGenerator) {
console.log(`Processing ${pageData.length} items:`);
console.log(pageData);
console.log('---');
}
}
// Usage
processAllData();
Example 2: Task Scheduler
A simple task scheduler using generators:
function* scheduler() {
let queue = [];
while (true) {
// Get a new task or process existing ones
const newTask = yield queue.length > 0 ? execute(queue.shift()) : 'Idle';
if (newTask) {
queue.push(newTask);
}
}
}
function execute(task) {
return `Executed: ${task}`;
}
const taskManager = scheduler();
console.log(taskManager.next().value); // Idle
console.log(taskManager.next('Task 1').value); // Executed: Task 1
console.log(taskManager.next('Task 2').value); // Executed: Task 2
console.log(taskManager.next().value); // Idle
Summary
JavaScript generators provide a powerful way to control function execution flow, allowing you to:
- Pause and resume function execution
- Create iterators easily
- Generate potentially infinite sequences
- Process data on-demand (lazy evaluation)
- Improve code readability for complex sequential operations
Generators shine in scenarios where you need to work with sequences of values, asynchronous operations, or complex control flows. They bridge the gap between synchronous and asynchronous code, making certain programming patterns more elegant and maintainable.
Further Learning
To deepen your understanding of generators, try these exercises:
- Create a generator that yields random numbers within a specified range
- Implement a generator that traverses a tree structure
- Use generators to implement custom iteration for your own data types
- Build an async generator that fetches data from an API in chunks
Additional Resources
Generators might seem complex at first, but they open up new possibilities for writing clean, efficient code, especially when dealing with data streams or asynchronous operations.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)