Generators and Iterators in JavaScript

· 6 min read · Updated March 13, 2026 · intermediate
javascript es6 generators iterators

Generators are a powerful ES6 feature that lets you create functions capable of pausing execution and producing values on demand. Unlike regular functions that run to completion, generators can yield multiple values over time, suspend between those yields, and resume later. This makes them perfect for working with sequences, lazy evaluation, and async 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.

You can also use generators as methods in classes or object literals:

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—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—they return this from their [Symbol.iterator]() method:

function* gen() {
  yield 1;
}

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

This is why you can only iterate a generator once. After the for...of loop consumes it, calling next() again returns { value: undefined, done: true }.

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’s just 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

As shown earlier, generators can produce infinite sequences without memory issues:

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

Generators make it easy to create custom iterable objects:

const range = {
  from: 1,
  to: 5,
  
  *[Symbol.iterator]() {
    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 }

Using return() terminates the generator immediately:

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

A common mistake is forgetting that generators are single-use. Once you iterate through a generator, it’s exhausted:

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 expecting yield to work in regular functions. It doesn’t. Only generator functions can use yield:

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

See Also