How JavaScript Generators and Yield Work

Generator functions and the yield statement provide a unique way to control execution flow in JavaScript by allowing a function to pause its execution and resume it later. Unlike standard functions that run to completion and return a single value, generator functions produce a sequence of values on demand. This article explains the syntax of generator functions, how yield pauses and passes data, how the .next() method controls execution, and the practical use cases for these tools.

Defining a Generator Function

A generator function is declared using the function* syntax (an asterisk following the function keyword). When invoked, a generator function does not execute its body immediately. Instead, it returns a special object called a Generator object, which conforms to both the iterable and iterator protocols.

function* simpleGenerator() {
  console.log("Execution started");
  yield 1;
  console.log("Execution resumed");
  yield 2;
}

const gen = simpleGenerator(); // Does not run the code yet

The Role of the yield Statement

The yield keyword is used inside a generator function to pause execution and emit a value to the caller. When the engine encounters a yield, the function’s state (including local variables and execution position) is frozen, and control returns to the calling context.

Each time execution is paused at a yield, the generator returns an object with two properties: - value: The data following the yield keyword. - done: A boolean indicating whether the generator has finished (false if there are more yields, true if the function has ended or returned).

Controlling Execution with .next()

To start or resume a generator, you call the .next() method on the generator object. Execution proceeds until it hits the next yield or return statement.

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 }

Two-Way Communication: Passing Arguments to .next()

The yield expression not only produces values but can also receive values. When you pass an argument to .next(value), that argument becomes the result of the yield expression currently paused inside the function.

function* conversation() {
  const reply = yield "What is your name?";
  yield `Hello, ${reply}!`;
}

const chat = conversation();
console.log(chat.next().value);       // "What is your name?"
console.log(chat.next("Alice").value); // "Hello, Alice!"

Note: The first call to .next() cannot pass a value because it merely starts the generator up to the first yield.

Delegating Generators with yield*

The yield* expression allows a generator function to delegate its iteration to another generator or iterable object (such as an Array).

function* subRoutine() {
  yield "A";
  yield "B";
}

function* mainRoutine() {
  yield 1;
  yield* subRoutine();
  yield 2;
}

const iterator = mainRoutine();
// Yields: 1, "A", "B", 2

Terminating and Handling Errors

Generators have built-in methods for handling exceptions and early termination:

function* cancellable() {
  try {
    yield "Working...";
  } catch (err) {
    console.log("Caught:", err);
  }
}

const task = cancellable();
task.next();
task.throw(new Error("Stop task")); // Triggers the catch block

Common Use Cases

  1. Lazy Evaluation and Infinite Sequences: Generators compute values only when requested, making them ideal for generating large or infinite data sets (such as unique IDs or Fibonacci numbers) without consuming excessive memory.
  2. Custom Iterables: You can attach a generator function to an object’s [Symbol.iterator] method to make the object directly iterable with for...of loops.
  3. State Machines: Because generators maintain their internal state between calls, they are effective tools for modeling complex state transitions cleanly.