Skip to main content

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.

javascript
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

  1. When we call simpleGenerator(), it doesn't execute the function body immediately. Instead, it returns a generator object.

  2. The function only starts executing when we call next() on the generator object.

  3. The function runs until it encounters a yield statement, at which point it pauses and returns an object with two properties:

    • value: The yielded value
    • done: A boolean indicating whether the generator is finished
  4. Each time we call next(), the function resumes execution from where it was paused until it reaches the next yield or finishes.

Generator Iteration

Generators are iterable, which means we can use them in for...of loops or spread them into arrays:

javascript
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:

javascript
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:

javascript
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:

javascript
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:

javascript
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:

javascript
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:

javascript
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:

  1. Create a generator that yields random numbers within a specified range
  2. Implement a generator that traverses a tree structure
  3. Use generators to implement custom iteration for your own data types
  4. 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! :)