jsguides

JavaScript Generators and Iterators

JavaScript generators and iterators arrived with ES6 and changed how JavaScript handles sequences. A generator is a function that can pause execution and produce values on demand. Unlike regular functions that run to completion, generators can yield multiple values over time, suspend between yields, and resume later. This makes them ideal for lazy evaluation, infinite sequences, and custom iteration patterns.

What Is a Generator?

A generator is a function that can pause its execution and resume later. When you call a generator, it doesn’t run immediately. Instead, it returns a generator object that controls the execution.

The key difference from regular functions is the function* syntax and the yield keyword:

function* countToThree() {
  yield 1;
  yield 2;
  yield 3;
}

const counter = countToThree();

console.log(counter.next()); // { value: 1, done: false }
console.log(counter.next()); // { value: 2, done: false }
console.log(counter.next()); // { value: 3, done: false }
console.log(counter.next()); // { value: undefined, done: true }

Each call to next() resumes execution until the next yield, then pauses again. The object returned by next() always has two properties: value (the yielded value) and done (whether the generator has finished).

Generator function syntax

The asterisk goes after the function keyword. That’s it. You can put it anywhere between function and the function name:

// All of these are valid
function* myGenerator() {}
function *myGenerator() {}
function * myGenerator() {}

Most JavaScript style guides prefer function* name() with no space, so stick with that for consistency.

Beyond standalone functions, you can define generator methods directly inside object literals and classes. The syntax is the same: prefix the method name with *. A generator method behaves like any other generator, yielding values on each next() call, which makes it a clean way to add custom iteration to your own objects:

const obj = {
  *generatorMethod() {
    yield 1;
    yield 2;
  }
};

class MyClass {
  *generatorMethod() {
    yield 'hello';
  }
}

Understanding the yield keyword

The yield keyword is what makes generators special. It pauses the function and specifies the value to return. When you call next() on the generator, execution resumes right after the previous yield.

Here’s a generator that produces an infinite sequence:

function* naturalNumbers() {
  let n = 1;
  while (true) {
    yield n;
    n++;
  }
}

const numbers = naturalNumbers();
console.log(numbers.next().value); // 1
console.log(numbers.next().value); // 2
console.log(numbers.next().value); // 3

This works because the generator only computes the next value when next() is called. It doesn’t try to generate an infinite array, which would crash your program.

You can also use yield* to delegate to another generator or iterable:

function* gen1() {
  yield 1;
  yield 2;
}

function* gen2() {
  yield* gen1();
  yield 3;
}

const gen = gen2();
console.log(gen.next().value); // 1
console.log(gen.next().value); // 2
console.log(gen.next().value); // 3

This is useful for composing generators or flattening nested generator calls.

Generators implement the iterator protocol

Every generator object is also an iterator. It implements the iterator protocol by having a next() method that returns { value, done }. But generators go further, and they also implement the iterable protocol.

This means generators work with for...of loops, the spread operator, and destructuring:

function* colors() {
  yield 'red';
  yield 'green';
  yield 'blue';
}

// Works with for...of
for (const color of colors()) {
  console.log(color);
}

// Works with spread
const colorArray = [...colors()];

// Works with destructuring
const [first, second] = colors();

Generators are self-iterable: their Symbol.iterator method returns the generator itself rather than creating a new iterator. This design is why for...of and spread work directly on a generator call without any extra wrapping or conversion. Verify this self-reference by checking identity:

function* gen() {
  yield 1;
}

const generator = gen();
console.log(generator[Symbol.iterator].call(generator) === generator); // true

Because the generator is its own iterator, you can only iterate it once. After for...of consumes all values, subsequent next() calls return { value: undefined, done: true }. If you need to replay the sequence, call the generator factory again to get a fresh instance.

Passing values into generators

The next() method can accept a value, which becomes the result of the yield expression. This lets you communicate back into the generator:

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

  while (true) {
    const reset = yield current;
    [current, next] = [next, current + next];

    if (reset) {
      current = 0;
      next = 1;
    }
  }
}

const fib = fibonacci();
console.log(fib.next().value);   // 0
console.log(fib.next().value);   // 1
console.log(fib.next().value);   // 1
console.log(fib.next().value);   // 2
console.log(fib.next().value);   // 3
console.log(fib.next(true).value); // 0 (reset!)

The first next() call always starts the generator without passing a value. That is how the protocol works: the value you pass to next() becomes the result of the yield that was paused.

Practical use cases

Lazy Evaluation

Generators compute values only when needed. This is useful for expensive computations:

function* fetchPages(urls) {
  for (const url of urls) {
    const response = yield fetch(url);
    yield response.json();
  }
}

// Only fetches the first URL until you call next() again
const fetcher = fetchPages(['/api/users', '/api/posts', '/api/comments']);
const userResponse = await fetcher.next().value;
const users = await fetcher.next().value;

This pattern lets you control the flow of expensive operations and avoid loading everything into memory at once.

Infinite sequences

Lazy evaluation also means generators never compute values that aren’t requested. An infinite sequence like powers of two costs nothing to define because each value is computed on demand, one next() call at a time:

function* powersOfTwo() {
  let power = 1;
  while (true) {
    yield power;
    power *= 2;
  }
}

const powers = powersOfTwo();
for (let i = 0; i < 10; i++) {
  console.log(powers.next().value); // 1, 2, 4, 8, 16, 32, 64, 128, 256, 512
}

The sequence exists only as a “promise” of values. Each value is computed on demand.

Implementing custom iterables

Beyond consuming built-in iterables, generators let you define iteration behavior for your own objects. A single *[Symbol.iterator] method replaces the manual boilerplate of tracking state, returning { value, done } objects, and handling the done condition:

const range = {
  from: 1,
  to: 5,

  [Symbol.iterator]: function* () {
    for (let i = this.from; i <= this.to; i++) {
      yield i;
    }
  }
};

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

This is much cleaner than manually implementing the iterator protocol with next() methods.

Generator Methods

Generator objects have three built-in methods:

  • next(value): resumes execution, optionally passing a value
  • throw(error): throws an error at the paused yield
  • return(value): immediately finishes the generator with a value
function* gen() {
  try {
    yield 1;
    yield 2;
  } catch (e) {
    console.log('Caught:', e);
  }
}

const g = gen();
g.next();           // { value: 1, done: false }
g.throw(new Error('oops')); // Caught: Error: oops
g.next();           // { value: undefined, done: true }

The throw() method injects an error at the current yield point, which the generator can catch with a try/catch block. After catching, the generator may continue or terminate depending on what the catch block does.

Calling return() skips past all remaining yields and forces the generator into the done state immediately. The argument you pass becomes the final value, which is useful for early termination in iterative workflows:

function* gen() {
  yield 1;
  yield 2;
  yield 3;
}

const g = gen();
g.next();        // { value: 1, done: false }
g.return('done'); // { value: 'done', done: true }
g.next();        // { value: undefined, done: true }

Common Mistakes

The most frequent pitfall with generators is treating them as reusable. A generator object is single-use: once all values have been consumed, further next() calls return { done: true } indefinitely. If your code spreads a generator into an array and then tries to spread it again, the second spread produces nothing:

function* numbers() {
  yield 1;
  yield 2;
}

const nums = numbers();
console.log([...nums]); // [1, 2]
console.log([...nums]); // [] empty!

If you need to iterate multiple times, create a new generator each time or convert to an array first.

Another mistake is using yield outside a generator function. The yield keyword is only valid inside function* bodies. Using it in a regular function, an arrow function, or a callback produces a syntax error:

// SyntaxError: Unexpected number
function regularFunction() {
  yield 1;
}

Generators as state machines

A generator is often easiest to understand as a small state machine. Each yield marks a stable point, and each next() call chooses the next path forward. That mental model helps when you use generators for staged workflows, input handling, or work that needs to pause and continue later.

Reuse generator factories

Do not try to reuse a finished generator object. Create a fresh generator each time you need the sequence again. That keeps the code honest about where the data comes from and prevents exhausted iterators from masquerading as live ones.

Yield work in small pieces

Generators are a practical way to break a long operation into slices. Each step can compute a little, yield a result, and then continue when the caller is ready. That makes them useful for custom iteration, simple schedulers, and any place where you want pull-based flow instead of immediate execution.

Debugging control flow

When a generator behaves oddly, inspect which call resumed it last and what value was passed back in. Bugs often come from assuming the first next() can carry data or from forgetting that yield is also an expression. Reading the flow step by step usually makes the problem obvious.

Pull-based flow is the point

Generators work well when the caller should decide when the next value is needed. That keeps expensive work from starting too early and lets you pause between steps without building a custom scheduler. Use that pattern when you want control over pacing.

Keep the sequence readable

A generator is easier to maintain when each yield has a clear meaning. If a function starts handling too many states, the sequence becomes harder to follow. In that case, split it into smaller generators or move shared setup outside the loop.

Use names that reflect state

Good names help a generator feel like a sequence instead of a puzzle. If a yield represents loading, waiting, or finishing, name the step so the next reader can see the intent without tracing every branch. Clear names make the control flow feel much calmer.

Keep side effects visible

Generators are easier to trust when their side effects are obvious. If a step talks to the network, touches the file system, or updates shared state, keep that action easy to spot in the body of the function. That makes the pauses and resumes much easier to reason about.

Make resets obvious

If a generator can restart or loop, make the reset point easy to spot in the code. Clear boundaries between one pass and the next keep the control flow easier to debug and make reused generator factories feel much more predictable.

See Also