ECMAScript Generator

Introduced in ECMAScript 2015 (ES6), Generators are a powerful addition to the JavaScript language. Unlike standard functions, which execute from start to finish once called, generators can be paused in the middle of their execution and resumed later. This unique capability allows them to produce a sequence of values over time, making them indispensable for handling asynchronous flows, managing large data sets, and implementing custom iteration logic.
A generator function is declared using the function* syntax (note the asterisk). Inside these functions, the yield keyword is used to pause execution and send a value back to the caller.

When you call a generator function, it does not immediately execute the code inside the block. Instead, it returns a Generator Object. This object conforms to both the iterable and iterator protocols, meaning it can be traversed using a loop or manually via the .next() method.

function* simpleGenerator() {
  yield 1;
  yield 2;
  return 3;
}

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

Each call to .next() resumes the function from where it was last paused until it encounters the next yield or a return statement. The returned object contains two properties: value (the yielded value) and done (a boolean indicating if the generator has finished).

Two-Way Communication

One of the most sophisticated features of generators is their ability to receive values from the caller. In JavaScript, yield is not just a statement—it is an expression. This means it can evaluate to a value.

When you pass an argument to the .next(value) method, that value is injected back into the generator as the result of the yield expression where the function was paused.

function* echo() {
  const name = yield 'Please enter your name:';
  yield `Hello, ${name}!`;
}

const it = echo();
console.log(it.next());        // { value: 'Please enter your name:', done: false }
console.log(it.next('Alice')); // { value: 'Hello, Alice!', done: false }

Important Note: The first call to .next() cannot pass a value into the generator because the function hasn't reached its first yield yet. The first argument is effectively ignored.

Iterators and the Iterable Protocol

Because generator objects implement the [Symbol.iterator] method (which simply returns the generator itself), they are fully compatible with JavaScript's iteration ecosystem. You can use them directly with for...of loops or the spread operator (...).

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

// Using the spread operator to convert the generator to an array
console.log([...range(1, 5)]); // [1, 2, 3, 4, 5]

// Using for...of for efficient iteration
for (const n of range(1, 3)) {
  console.log(n); // 1, 2, 3
}

Generator Delegation with yield*

The yield* expression allows a generator to delegate part of its operations to another iterable object (such as an array, a string, or another generator). This is particularly useful for composing complex generators from smaller, modular ones.

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

function* outer() {
  yield 0;
  yield* inner(); // Delegates to the inner generator
  yield 1;
}

console.log([...outer()]); // [0, 'a', 'b', 1]

Advanced Control: return() and throw()

Beyond .next(), generator objects provide two other critical methods for external control:

  1. .return(value): Immediately forces the generator to complete. If the generator is wrapped in a try...finally block, the finally block will still execute, ensuring resources are cleaned up.
  2. .throw(error): Injects an error into the generator at the current pause point. This allows the generator to handle the error internally using a try...catch block.
function* task() {
  try {
    yield 1;
    yield 2;
  } finally {
    console.log('Cleaning up resources...');
  }
}

const t = task();
console.log(t.next());     // { value: 1, done: false }
console.log(t.return(99)); // Logs "Cleaning up resources...", returns { value: 99, done: true }

Async Generators

Introduced in ES2018, Async Generators (async function*) combine the power of generators with async/await. Instead of returning a value immediately, the .next() method of an async generator returns a Promise. These are typically consumed using the for await...of loop.

async function* asyncSeq() {
  yield 1;
  yield await Promise.resolve(2);
  yield 3;
}

(async () => {
  for await (const v of asyncSeq()) {
    console.log(v); // 1, 2, 3
  }
})();

Async generators are ideal for handling data streams, such as paginated API requests or reading large files in chunks.

Common Use Cases

  • Infinite Sequences: Generators can produce an endless stream of values (e.g., Fibonacci numbers or unique IDs) without crashing the program, as values are only computed on demand.
  • Lazy Evaluation: By calculating values only when .next() is called, generators save memory and CPU cycles.
  • Custom Iterators: They provide a clean way to define how complex data structures should be traversed.
  • State Machines: The pause-and-resume nature of generators makes them perfect for representing state transitions.
  • Asynchronous Flow Control: Before async/await became standard, generators combined with Promises (via libraries like co) were the primary way to write "synchronous-looking" asynchronous code.

Key Constraints and Caveats

To use generators effectively, keep the following rules in mind:

  • No new keyword: Generator functions cannot be used as constructors.
  • No Arrow Functions: You cannot declare a generator using arrow function syntax.
  • Scope of yield: The yield keyword can only be used inside a generator function; it cannot be used inside a regular nested function within a generator.
  • Single-Use: Generator objects are "exhaustible." Once they reach the done: true state, they cannot be reset or restarted. To iterate again, you must create a new generator object by calling the generator function once more.