Academic Block

Generators & Iterators Get Certified

Generators and iterators give you fine-grained control over how sequences of values are produced and consumed. Instead of building an entire array in memory, you can produce values one at a time, pause execution, and even send values back into a running function.

What Is an Iterator?

An iterator is any object that implements a next() method. Each call to next() returns an object with two properties: value (the next item) and done (a boolean that becomes true when there are no more items).

What this code does: Manually creates a simple iterator object that yields three numbers and then signals it is finished.

const numbers = {
    data: [10, 20, 30],
    index: 0,
    next() {
        if (this.index < this.data.length) {
            return { value: this.data[this.index++], done: false };
        }
        return { value: undefined, done: true };
    }
};

console.log(numbers.next());
console.log(numbers.next());
console.log(numbers.next());
console.log(numbers.next());

Making an Object Iterable with Symbol.iterator

An object becomes iterable when it has a method keyed by Symbol.iterator. That method must return an iterator. Once an object is iterable, you can use it with for...of, the spread operator, and more.

What this code does: Adds Symbol.iterator to a custom object so it works with for...of and the spread operator.

const range = {
    from: 1,
    to: 4,
    [Symbol.iterator]() {
        let current = this.from;
        const last = this.to;
        return {
            next() {
                if (current <= last) {
                    return { value: current++, done: false };
                }
                return { done: true };
            }
        };
    }
};

for (const num of range) {
    console.log(num);
}

console.log([...range]);

Generator Functions (function*)

A generator function is written with function*. Inside it, the yield keyword pauses execution and returns each value automatically.

What this code does: A generator function yields a countdown from a starting number down to zero. Each call to next() advances one step.

function* countdownGenerator(start) {
    for (let i = start; i >= 0; i--) {
        yield i;
    }
}

const countdown = countdownGenerator(3);

console.log(countdown.next());
console.log(countdown.next());
console.log(countdown.next());
console.log(countdown.next());
console.log(countdown.next());

Calling a generator function doesn’t run its body immediately — it returns a special generator object. Each call to next() runs the function until it hits a yield, pauses there, and hands back that value.

Generators Work Directly With for...of

Because a generator object is automatically iterable, you can use it directly in a for...of loop without any extra setup.

What this code does: A generator yields weekday names one by one. Because generators are iterable, you can loop over them directly with for...of.

function* weekdays() {
    yield "Monday";
    yield "Tuesday";
    yield "Wednesday";
    yield "Thursday";
    yield "Friday";
}

for (const day of weekdays()) {
    console.log(day);
}

You can also spread a generator directly into an array.

What this code does: Spreads the values produced by a generator into a real array using the spread operator.

function* weekdays() {
    yield "Monday";
    yield "Tuesday";
    yield "Wednesday";
}

const days = [...weekdays()];
console.log(days);

The done Value and return Inside a Generator

A return statement inside a generator ends it, and that returned value comes back with done: true. It is not included by a for...of loop, which stops as soon as it sees done: true.

What this code does: Shows that a return inside a generator finishes the sequence and supplies a final value (with done: true) that for...of does not include.

function* deliveryStatus() {
    yield "Order placed";
    yield "Shipped";
    return "Delivered";
}

const status = deliveryStatus();

console.log(status.next());
console.log(status.next());
console.log(status.next());

Passing Values Into next()

A value passed to next() becomes the result of the yield expression that was paused, allowing two-way communication with a running generator.

What this code does: Demonstrates two-way communication: values you pass to next() become the result of the paused yield expression.

function* orderTaker() {
    const firstItem = yield "What would you like to order?";
    const secondItem = yield "Anything else, after " + firstItem + "?";
    return "Order complete: " + firstItem + " and " + secondItem;
}

const order = orderTaker();

console.log(order.next());
console.log(order.next("coffee"));
console.log(order.next("a croissant"));

The first call to next() can’t pass in a useful value yet, since the generator hasn’t reached its first yield to receive it.

Infinite Generators

Since a generator only computes a value when next() is actually called, it can represent a sequence that never ends — something that would be impossible to store as a regular array.

What this code does: Creates an infinite generator that yields even numbers on demand. Values are computed only when requested.

function* evenNumbers() {
    let current = 0;
    while (true) {
        yield current;
        current += 2;
    }
}

const evens = evenNumbers();

console.log(evens.next().value);
console.log(evens.next().value);
console.log(evens.next().value);
console.log(evens.next().value);

This works safely because nothing is computed until it’s asked for — looping over this generator with a plain for...of without a break would run forever, so infinite generators are usually paired with a limit.

Delegating to Another Generator With yield*

yield* delegates to another iterable, yielding each of its values in turn, which is useful for composing smaller generators into a larger one.

What this code does: Uses yield* to compose smaller generators into one larger sequence that yields every dish in order.

function* appetizers() {
    yield "Bruschetta";
    yield "Calamari";
}

function* mainCourses() {
    yield "Grilled Salmon";
    yield "Mushroom Risotto";
}

function* fullMenu() {
    yield* appetizers();
    yield* mainCourses();
    yield "Tiramisu";
}

for (const dish of fullMenu()) {
    console.log(dish);
}

Practical Example: A Custom Range Generator

Let’s build a reusable generator that produces a numeric range with a custom step, similar to Python’s range().

What this code does: A practical range generator (like Python’s range) that yields numbers from start up to (but not including) end with a custom step.

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

for (const value of range(0, 10, 2)) {
    console.log(value);
}

console.log([...range(5, 20, 5)]);

When to Reach for Generators

Situation Why a Generator Helps
Large or infinite sequences Values are produced one at a time, not stored all at once
Custom iteration logic Makes an object usable with for...of and spread
Step-by-step processes yield naturally pauses and resumes multi-step logic
Composable sequences yield* combines smaller generators into larger ones

Common Mistakes With Generators and Iterators

  • Forgetting the asterisk (*) after function, which silently turns it into a regular function where yield isn’t valid.
  • Looping over an infinite generator with a plain for...of and no break, causing the program to hang.
  • Expecting the value passed to the very first next() call to be used, when the generator hasn’t reached a yield to receive it yet.
  • Assuming a generator’s return value shows up in a for...of loop, when it is actually excluded once done becomes true.
  • Reusing an already-finished generator object, expecting it to start over — generators can only be iterated through once.

Best Practices for Generators and Iterators

  • Prefer generator functions over manually written iterator objects whenever possible — they’re far less error-prone.
  • Use yield* to compose generators instead of duplicating logic across them.
  • Always pair an infinite generator with a limiting condition, such as a counter or a break inside the loop consuming it.
  • Implement Symbol.iterator on custom objects when you want them to work naturally with for...of and the spread operator.
  • Create a new generator instance each time you need to iterate from the beginning, rather than reusing a finished one.

Quick Summary

  • An iterator is an object with a next() method returning { value, done }.
  • An object becomes iterable by implementing Symbol.iterator, enabling for...of and the spread operator.
  • Generator functions (function*) automatically build an iterator using yield to pause and resume.
  • yield* delegates to another iterable, combining sequences together.
  • Generators can represent infinite sequences safely, since values are only computed on demand.
  • Values can flow both out (via yield) and in (via arguments to next()) during a generator’s execution.

Try It Yourself

Modify the generator below. Try changing the starting values, or adding a limit so it doesn’t run forever when consumed with a for...of loop.

What this code does: A practice Fibonacci generator that yields the first limit numbers of the sequence. Experiment by changing the limit or the starting values.

function* fibonacci(limit) {
    let previous = 0;
    let current = 1;

    for (let i = 0; i < limit; i++) {
        yield previous;
        [previous, current] = [current, previous + current];
    }
}

console.log([...fibonacci(8)]);
Tip: If you’re about to write an object with a Symbol.iterator method that manually tracks an index and returns { value, done } by hand, try rewriting it as a generator function first — it’s almost always shorter and easier to follow.

Understanding generators and iterators is useful before moving on to asynchronous iteration, Proxies, Reflect, and more advanced JavaScript concepts.

🧪 Test Your JavaScript Code

Edit the code on the left and click "Run Code" to see the console output on the right.

📝 JavaScript Code
👁 Console Output

Click "Run Code" to see the output here.

Ctrl + Enter to run code Esc to close