Episode 5 of 5

Generators

Learn about JavaScript generator functions — pausable functions that use function* and yield for elegant control flow and async patterns.

Generators

Generators are a special type of function introduced in ES6 that can be paused and resumed. Unlike regular functions that run to completion, a generator function can yield a value, pause execution, and resume later from where it left off. This unique ability makes generators powerful for iterative data processing and async control flow.

What Is a Generator?

A generator function is declared with an asterisk (*) after the function keyword, and uses the yield keyword to pause execution:

function* myGenerator() {
    console.log('Step 1');
    yield 'first value';

    console.log('Step 2');
    yield 'second value';

    console.log('Step 3');
    return 'final value';
}

Calling a generator function does not execute its body. Instead, it returns a generator object — an iterator that you control by calling .next().

Using a Generator

const gen = myGenerator();

console.log(gen.next());
// Step 1
// { value: 'first value', done: false }

console.log(gen.next());
// Step 2
// { value: 'second value', done: false }

console.log(gen.next());
// Step 3
// { value: 'final value', done: true }

Each call to .next() runs the generator until it hits the next yield or return statement. It returns an object with two properties:

PropertyMeaning
valueThe yielded or returned value
donefalse if there are more yields; true if the generator has finished

How Yield Works

function* counter() {
    let count = 0;

    while (true) {
        count++;
        yield count;
    }
}

const gen = counter();

console.log(gen.next().value);  // 1
console.log(gen.next().value);  // 2
console.log(gen.next().value);  // 3
// Can continue indefinitely...

The generator remembers its state between calls. The count variable persists across .next() calls because execution is paused, not restarted. This infinite counter would be impossible with a regular function but works perfectly with a generator.

Passing Values Into a Generator

You can send values back into a generator through the .next(value) argument. The sent value becomes the result of the yield expression:

function* conversation() {
    const name = yield 'What is your name?';
    const age = yield 'How old are you, ' + name + '?';
    return name + ' is ' + age + ' years old';
}

const gen = conversation();

console.log(gen.next());
// { value: 'What is your name?', done: false }

console.log(gen.next('Alice'));
// { value: 'How old are you, Alice?', done: false }

console.log(gen.next('25'));
// { value: 'Alice is 25 years old', done: true }

The first .next() starts the generator and runs to the first yield. The second .next('Alice') resumes execution and passes 'Alice' as the value of the first yield expression, which gets assigned to name.

Generators as Iterables

Generator objects are iterable, so you can use them with for...of loops, the spread operator, and destructuring:

function* range(start, end) {
    for (let i = start; i <= end; i++) {
        yield i;
    }
}

// for...of loop
for (const num of range(1, 5)) {
    console.log(num);
}
// 1, 2, 3, 4, 5

// Spread operator
const numbers = [...range(1, 5)];
// [1, 2, 3, 4, 5]

// Destructuring
const [a, b, c] = range(10, 20);
// a=10, b=11, c=12

yield* — Delegating to Another Generator

function* innerGen() {
    yield 'a';
    yield 'b';
}

function* outerGen() {
    yield 1;
    yield* innerGen();  // Delegate to innerGen
    yield 2;
}

const gen = outerGen();
console.log(gen.next().value);  // 1
console.log(gen.next().value);  // 'a'
console.log(gen.next().value);  // 'b'
console.log(gen.next().value);  // 2

yield* delegates to another generator (or any iterable), yielding each of its values before continuing with the outer generator. This lets you compose generators from smaller pieces.

Generators for Async Control Flow

Generators can work with Promises to create synchronous-looking asynchronous code. The idea is to yield a Promise, wait for it to resolve, and then send the result back into the generator:

function* fetchUserFlow() {
    const user = yield fetchData('/api/users/1');
    console.log('User:', user.name);

    const posts = yield fetchData('/api/posts?userId=' + user.id);
    console.log('Posts:', posts.length);
}

// Runner function that handles the Promise-yielding pattern
function run(generatorFn) {
    const gen = generatorFn();

    function step(value) {
        const result = gen.next(value);
        if (result.done) return;

        result.value
            .then(data => step(data))
            .catch(err => gen.throw(err));
    }

    step();
}

run(fetchUserFlow);

The run function is a generator runner. It calls .next() to get a Promise, waits for it to resolve with .then(), and passes the result back into the generator with the next .next(data) call. The generator code reads like synchronous code even though it is fully asynchronous.

Error Handling in Generators

function* safeFlow() {
    try {
        const data = yield fetchData('/api/invalid-endpoint');
        console.log(data);
    } catch (err) {
        console.log('Caught error:', err);
    }
}

// The runner uses gen.throw(err) to send errors into the generator
// Which triggers the catch block inside the generator

You can use standard try/catch blocks inside generators. The runner calls gen.throw(err) when a yielded Promise rejects, which throws the error inside the generator at the point where it is paused. This is a huge improvement over callback error handling.

Generators vs Other Async Patterns

FeatureCallbacksPromisesGenerators
ReadabilityNested (callback hell)Flat chains (.then)Synchronous-looking
Error handlingPer-callback if/elseSingle .catch()Standard try/catch
DebuggingHard (anonymous functions)Better (named chains)Easy (line-by-line stepping)
CancellationNot built-inNot built-ingen.return() stops execution
Requires runnerNoNoYes (external function)

Practical Use Cases for Generators

  • Lazy sequences — generate values on demand without creating large arrays in memory
  • Infinite data streams — ID generators, Fibonacci sequences, pagination
  • State machines — each yield represents a state transition
  • Custom iterators — make any object iterable with a generator
  • Async control flow — the foundation that inspired async/await

Practical Example: ID Generator

function* idGenerator(prefix = 'id') {
    let id = 1;
    while (true) {
        yield prefix + '-' + id;
        id++;
    }
}

const userIds = idGenerator('user');
const postIds = idGenerator('post');

console.log(userIds.next().value);  // "user-1"
console.log(userIds.next().value);  // "user-2"
console.log(postIds.next().value);  // "post-1"
console.log(userIds.next().value);  // "user-3"

Practical Example: Paginated Data

function* paginate(items, pageSize) {
    for (let i = 0; i < items.length; i += pageSize) {
        yield items.slice(i, i + pageSize);
    }
}

const allItems = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const pages = paginate(allItems, 3);

console.log(pages.next().value);  // [1, 2, 3]
console.log(pages.next().value);  // [4, 5, 6]
console.log(pages.next().value);  // [7, 8, 9]
console.log(pages.next().value);  // [10]
console.log(pages.next().done);   // true

Generators and async/await

The generator + runner pattern was so popular that JavaScript formalized it as async/await in ES2017. Under the hood, async/await works very similarly to a generator yielding Promises with an automatic runner:

// Generator + runner (manual)
function* fetchFlow() {
    const user = yield fetchData('/api/users/1');
    const posts = yield fetchData('/api/posts?userId=' + user.id);
    console.log(posts);
}
run(fetchFlow);

// async/await (built-in runner)
async function fetchFlow() {
    const user = await fetchData('/api/users/1');
    const posts = await fetchData('/api/posts?userId=' + user.id);
    console.log(posts);
}
fetchFlow();

Replace function* with async function, yield with await, and the runner is no longer needed — the language handles it for you. Understanding generators helps you understand how async/await works under the surface.

Key Takeaways

  • Generators are functions declared with function* that can pause with yield and resume with .next()
  • Each .next() returns { value, done } — the yielded value and whether the generator has finished
  • You can send values into a generator with .next(value) — the value becomes the result of the yield expression
  • Generators are iterable — use them with for...of, spread, and destructuring
  • Yielding Promises and using a runner function creates synchronous-looking async code with standard try/catch error handling
  • Generators inspired async/await — understanding them helps you understand how async/await works internally
  • Practical uses include lazy sequences, ID generators, pagination, and custom iterators