Generators and Iterators

Learn how iterators and generator functions work in JavaScript. Understand the protocols, create custom iterators, and master generator methods like yield, return, and throw.

Loading...
Generators and Iterators

What is an Iterator?

An iterator is an object that lets you go through a list (or any collection) one item at a time.

It gives you the next item each time you ask, and remembers where it stopped, like a bookmark.

You don’t need to know how the collection is stored, just that you can use the iterator to step through it.

The Iterator Protocol

In JavaScript, an iterator is an object that implements the iterator protocol by having a next() method.

The next() method returns an object with two properties:

  • value: the next value from the list
  • done: a boolean indicating whether the iteration is complete

Example of Iterator

function createIterator(arr) {
  let index = 0;
  return {
    next: function () {
      if (index < arr.length) {
        return { value: arr[index++], done: false };
      } else {
        return { value: undefined, done: true };
      }
    },
  };
}
 
const numbers = createIterator([10, 20, 30]);
 
console.log(numbers.next()); // { value: 10, done: false }
console.log(numbers.next()); // { value: 20, done: false }
console.log(numbers.next()); // { value: 30, done: false }
console.log(numbers.next()); // { value: undefined, done: true }

Here,

  • You define a function createIterator that takes an array (arr) and returns an iterator object.
  • let index = 0; keep track of which element to return next.
  • return { next: function () { returns an object with a next() method, this follows the iterator protocol.
  • if (index < arr.length) {...} else {...}:
    • If there are more items in the array:
      • Return the current item and done: false
      • Move to the next item by increasing index
    • If there are no more items:
      • Return value: undefined and done: true
  • Using the Iterator:
    • const numbers = createIterator([10, 20, 30]); creates an iterator from the array [10, 20, 30].
  • Calling next():
    • console.log(numbers.next()); // { value: 10, done: false }:
      • Each call to next() gives the next value.
      • Once the end is reached, done: true tells you there’s nothing left.
  • Result: You get each item from the array, one at a time, in order, with full control over the process.

Why use it?

Custom iterators give you full control over how and when items are returned. It's great when working with custom data structures or when you want to pause/resume iteration.

The Symbol.iterator Protocol

You can make any object iterable by defining [Symbol.iterator]().

const range = {
  start: 1,
  end: 5,
  [Symbol.iterator]() {
    let current = this.start;
    const end = this.end;
    return {
      next() {
        if (current <= end) {
          return { value: current++, done: false };
        } else {
          return { done: true };
        }
      },
    };
  },
};
 
for (const num of range) {
  console.log(num); // Output: 1 2 3 4 5
}

Here,

  • range is a plain object with start and end properties.
  • [Symbol.iterator]() { ... }:
    • By adding a [Symbol.iterator]() method, you make range work with for...of.
  • Inside Symbol.iterator:
    • You initialize current to start from range.start (1).
    • end is the last number in the range (5).
  • Returning the iterator object: return {...};
    • This object has a next() method.
    • Each call to next() returns the next number and sets done to false.
    • When current exceeds end, it returns done: true.
  • Using in a for...of loop: for (const num of range) {...}
    • Because range now follows the iterable protocol, it can be looped with for...of.
  • Result: The object behaves like an array in loops, you can control how it yields values!

What is a Generator?

A generator function is a special type of function that can pause and resume its execution using the yield keyword.

Instead of returning all values at once, it produces a sequence of values over time, one at a time.

Syntax of Generator

function* generatorName() {
  yield value1;
  yield value2;
}
  • Notice the * after the function keyword, this marks it as a generator function.
  • yield is used to pause and send a value back each time the function runs.

Example:

function* countUp() {
  yield 1;
  yield 2;
  yield 3;
}
 
const counter = countUp();
 
console.log(counter.next().value); // Output: 1
console.log(counter.next().value); // Output: 2
console.log(counter.next().value); // Output: 3

Here,

  • countUp is a generator function that yields three values: 1, 2, and 3.
  • Calling countUp() does not run the function right away, it returns a generator object counter.
  • Calling counter.next():
    • Runs the function until the next yield.
    • Returns an object { value: X, done: false } where value is the yielded value.
  • counter.next().value gets the current yielded value.
  • Each call to .next() resumes where it last stopped.
  • After the last yield, .next() would return { value: undefined, done: true } indicating completion.

Use Cases of Generator

  • Custom Iteration Logic:

    Generators are useful when you want to:

    • Create custom iterators
    • Generate sequences step-by-step
    • Handle async flows (when used with async/await or Promises)

    Example: Iterating with for...of

    function* names() {
      yield "Alice";
      yield "Bob";
      yield "Charlie";
    }
     
    for (let name of names()) {
      console.log(name);
    }

    Here,

    • function* names() { ... }

      • This defines a generator function named names.
      • The * after function tells JavaScript that it’s a generator.
    • yield "Alice";, yield "Bob";, yield "Charlie";

      • Each yield gives out one value when the generator runs.
      • But instead of giving all values at once, it pauses after each yield.
    • names()

      • This doesn't run the code immediately.
      • It returns a generator object that can be used to get values one by one.
    • for (let name of names()) { ... }

      • The for...of loop that works well with generators. It automatically:
        • Starts the generator,
        • Gets the first yield value ("Alice"),
        • Then resumes to get "Bob",
        • Then resumes again to get "Charlie",
        • Finally, it stops when the generator is done.
    • console.log(name);

      • This line runs inside the loop each time a value is yielded.
      • It prints each name one by one.

    Output:

    Alice
    Bob
    Charlie

Generator Methods

1. next(value)

You can pass values back into a generator using .next(value). This makes generators interactive, useful for prompts, streams, or step-based workflows.

function* echo() {
  const input = yield "What's your name?";
  yield `Hello, ${input}!`;
}
 
const gen = echo();
console.log(gen.next()); // Output: { value: "What's your name?", done: false }
console.log(gen.next("Alice")); // Output: { value: "Hello, Alice!", done: false }
console.log(gen.next()); // Output: { value: undefined, done: true }

Here,

  • function* echo() defines a generator.
  • yield "What's your name?" pauses the function and sends the first message out.
  • const gen = echo(); creates the generator object, but doesn’t run the function yet.
  • console.log(gen.next()); starts the generator and runs until the first yield. So the output is { value: "What's your name?", done: false }.
  • console.log(gen.next("Alice")); resumes the generator, sending "Alice" as the result of the first yield and assigns "Alice" to input. So the output is { value: "Hello, Alice!", done: false }.
  • console.log(gen.next()); resumes the generator, no more yield statements left, so it finishes. So the output is { value: undefined, done: true }.

2. return(value)

The .return(value) method immediately stops the generator and sets the done flag to true. It also returns the provided value.

This is useful if you want to exit early from the generator before it yields all values.

function* numbers() {
  yield 1;
  yield 2;
  yield 3;
}
 
const gen = numbers();
console.log(gen.next()); // Output: { value: 1, done: false }
console.log(gen.return(42)); // Output: { value: 42, done: true }
console.log(gen.next()); // Output: { value: undefined, done: true }

Here,

  • First, .next() gives the first value 1.
  • Then, .return(42) forces the generator to finish immediately, returning { value: 42, done: true }.
  • Any further .next() calls after .return() just return { value: undefined, done: true }.

3. throw(error)

You can use .throw(error) to throw an error inside the generator at the current yield point.

If the generator has a try...catch block, the error can be caught and handled gracefully.

function* errorHandler() {
  try {
    yield 1;
    yield 2;
  } catch (e) {
    console.log("Caught:", e.message);
    yield "error handled";
  }
}
 
const gen = errorHandler();
console.log(gen.next()); // Output: { value: 1, done: false }
console.log(gen.throw(new Error("Oops!"))); // Logs: "Caught: Oops!", returns { value: "error handled", done: false }

Here,

  • gen.next() starts the generator and yields 1.
  • gen.throw(new Error("Oops!")) throws an error at the second yield.
  • The error is caught in the catch block, "Caught: Oops!" is logged.
  • The generator yields "error handled" as the next value.

Delegating to Other Generators

Sometimes, you want a generator to include values from another generator.

Instead of calling them one by one, use yield* to delegate control.

function* firstHalf() {
  yield 1;
  yield 2;
}
 
function* secondHalf() {
  yield 3;
  yield 4;
}
 
function* combined() {
  yield* firstHalf(); // Delegates to firstHalf()
  yield* secondHalf(); // Then delegates to secondHalf()
}
 
for (let num of combined()) {
  console.log(num); // Output: 1, 2, 3, 4
}

Here,

  • combined() doesn’t yield directly. It hands over control to firstHalf() using yield*.
  • After firstHalf() finishes, control goes to secondHalf() via another yield*.
  • The for...of loop receives values from all delegated generators in order.

👉 Next tutorial: Garbage Collection

Support my work!