JavaScript Generators Deep Dive: Full Guide

Generators are functions that can pause and resume execution, yielding values one at a time. Learn how function*, yield, and next() work, and when generators outperform regular functions.

7 min read

JavaScript generators are functions that can stop halfway through, return a value, and then pick up exactly where they left off when asked for the next value. Regular functions run from start to finish and return once. Generators can return many times, handing you one value per request.

You declare a generator with function* and use yield to pause and emit a value:

javascriptjavascript
function* countToThree() {
  yield 1;
  yield 2;
  yield 3;
}
 
const iterator = countToThree();
 
console.log(iterator.next()); // { value: 1, done: false }
console.log(iterator.next()); // { value: 2, done: false }
console.log(iterator.next()); // { value: 3, done: false }
console.log(iterator.next()); // { value: undefined, done: true }

Calling countToThree() does not run the function body. It returns an iterator object instead.

Each call to .next() runs the function until the next yield, then pauses. The return value is an object with a value property (the yielded value) and a done property (whether the generator is finished).

The Generator Lifecycle

A generator moves through distinct states during its lifetime:

Generator lifecycle states

When you call the generator function, it enters SuspendedStart. The body has not run yet. The first call to next moves it to Executing, and it runs until it hits a yield, which suspends it again.

Each subsequent call resumes from the last suspension point. When the function body finishes, whether by return or reaching the end, the generator enters Completed and all future calls return an object with value set to undefined and done set to true.

Yield and Two-Way Communication

yield does more than emit values. It can also receive them. The argument you pass to next() becomes the return value of the yield expression inside the generator:

javascriptjavascript
function* echo() {
  const first = yield "Send me a value";
  const second = yield `You sent: ${first}`;
  return `Final: ${first}, ${second}`;
}
 
const iter = echo();
 
console.log(iter.next().value);      // "Send me a value"
console.log(iter.next("Hello").value); // "You sent: Hello"
console.log(iter.next("World").value); // "Final: Hello, World"

The first next() call starts the generator and runs up to the first yield. At that point the generator pauses and returns the string "Send me a value".

The second call, next("Hello"), resumes execution, and the yield expression evaluates to "Hello", which gets assigned to the first variable. The third call does the same for the second variable.

This two-way channel makes generators useful for cooperative multitasking patterns, where the caller and the generator take turns passing data back and forth.

Yielding Other Iterables with yield*

The yield* expression delegates to another iterable, yielding each of its values one by one:

javascriptjavascript
function* flattened() {
  yield* [1, 2, 3];
  yield* [4, 5];
}
 
console.log([...flattened()]); // [1, 2, 3, 4, 5]

Without yield*, you would write a loop inside the generator. With it, you compose generators from other iterables cleanly. This also works with other generators, strings, Maps, and Sets.

The yield* expression can also receive values when the delegated generator supports two-way communication:

javascriptjavascript
function* inner() {
  const value = yield "inner waiting";
  return value.toUpperCase();
}
 
function* outer() {
  const result = yield* inner();
  yield result;
}
 
const it = outer();
console.log(it.next().value);    // "inner waiting"
console.log(it.next("hello").value); // "HELLO"

Practical Use Case 1: Lazy Sequences

Generators shine when the full sequence is too large or too expensive to compute all at once. A range generator computes numbers on demand:

javascriptjavascript
function* range(start, end) {
  for (let i = start; i <= end; i++) {
    yield i;
  }
}
 
// Only computes values as you iterate
for (const n of range(1, 1_000_000)) {
  if (n > 5) break;
  console.log(n);
}
// 1, 2, 3, 4, 5 -- never computed the other 999,995 values

An array of a million numbers would allocate memory for all of them upfront. The generator only computes the ones you actually consume.

Practical Use Case 2: Infinite Sequences

Generators can represent sequences that never end, which is impossible with arrays:

javascriptjavascript
function* fibonacci() {
  let a = 0, b = 1;
  while (true) {
    yield a;
    [a, b] = [b, a + b];
  }
}
 
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().value); // 5

The while (true) loop does not run forever. It pauses at each yield and only resumes when you call next. This is lazy evaluation in its purest form: the generator describes an infinite sequence, but only computes the finite slice you request.

You can iterate a controlled slice with a for...of loop combined with a break condition, or use a helper that takes the first N values.

Practical Use Case 3: Stateful Iteration

Generators hold their local state between pauses, which makes them natural for multi-step processes:

javascriptjavascript
function* wizard() {
  const name = yield "Step 1: What is your name?";
  const age = yield `Step 2: Hi ${name}, how old are you?`;
  yield `Step 3: ${name}, age ${age} -- complete!`;
}
 
const steps = wizard();
 
console.log(steps.next().value);        // Step 1: What is your name?
console.log(steps.next("Alice").value);  // Step 2: Hi Alice, how old are you?
console.log(steps.next("30").value);     // Step 3: Alice, age 30 -- complete!

Without a generator, you would manage this state manually with a step counter and a switch statement. The generator keeps the step logic linear and readable.

Generators and the Iterator Protocol

Generators automatically implement the iterator protocol. This means you can use them with for...of, the spread operator, and Array.from():

javascriptjavascript
function* letters() {
  yield "a";
  yield "b";
  yield "c";
}
 
// for...of
for (const letter of letters()) {
  console.log(letter);
}
 
// Spread
console.log([...letters()]); // ["a", "b", "c"]
 
// Array.from
console.log(Array.from(letters())); // ["a", "b", "c"]

For more on the iterator and iterable protocols, see /javascript/javascript-iterators-complete-guide and /javascript/javascript-iterable-protocol-complete-guide.

Generator Return and Throw

Generators support two additional control methods beyond next():

javascriptjavascript
function* demo() {
  try {
    yield 1;
    yield 2;
  } catch (err) {
    yield `Caught: ${err.message}`;
  }
  yield 3;
}
 
const it = demo();
console.log(it.next().value);             // 1
console.log(it.throw(new Error("oops")).value); // "Caught: oops"
console.log(it.next().value);             // 3

throw() injects an error at the current yield point. The generator can catch it and continue, as shown above. If the generator does not catch it, the error propagates to the caller.

return() forcibly ends the generator:

javascriptjavascript
const it2 = demo();
console.log(it2.next().value);    // 1
console.log(it2.return("done").value); // "done"
console.log(it2.next());           // { value: undefined, done: true }

These methods give the caller fine-grained control over generator execution without modifying the generator function itself.

Async Generators

An async generator combines async function* with yield and await to produce values from asynchronous sources:

javascriptjavascript
async function* fetchPages(urls) {
  for (const url of urls) {
    const response = await fetch(url);
    const data = await response.json();
    yield data;
  }
}
 
for await (const page of fetchPages(["/api/1", "/api/2"])) {
  console.log(page);
}

Each yield pauses the async generator while it awaits a network response. The consumer uses for await...of to iterate, which handles the promise unwrapping automatically.

Async generators are ideal for paginated APIs, streaming file reads, and any data source that arrives in chunks over time. For more on async patterns, see /javascript/javascript-async-await-guide-complete-tutorial.

When Not to Use Generators

Generators are not a drop-in replacement for arrays or regular functions:

  • Random access: You cannot jump to the nth element of a generator. Access is sequential only.
  • Reusable iteration: A generator iterator can only be traversed once. Call the generator function again to get a fresh iterator.
  • Simple mapping: If you just need to transform each element of a small array, .map() is more readable and often faster.
  • Single return value: If your function produces one result, a regular function is simpler. Generators add overhead for no benefit.

Use generators when you genuinely need lazy evaluation, infinite or very large sequences, two-way communication, or stateful multi-step processes. Otherwise, a regular function or array method is the clearer choice.

Rune AI

Rune AI

Key Insights

  • A generator is a function declared with function* that can pause execution with yield.
  • Calling a generator returns an iterator. Each next() call resumes execution until the next yield.
  • Generators are lazy: values are computed on demand, not upfront.
  • Use generators for infinite sequences, large data pipelines, and custom iteration logic.
  • Async generators (async function*) combine yield with await for streaming async data.
RunePowered by Rune AI

Frequently Asked Questions

What is the difference between a generator and a regular function?

A regular function runs to completion and returns a single value. A generator can pause mid-execution with yield, return multiple values over time, and resume where it left off when next() is called again.

When should I use a generator instead of an array?

Use a generator when the sequence is large, potentially infinite, or expensive to compute upfront. Generators compute values on demand, so you only pay for what you consume. If the sequence is small and you need random access, use an array.

Conclusion

Generators give you functions that can pause and resume, producing values lazily instead of all at once. They are the foundation for custom iterables, infinite sequences, and cooperative multitasking patterns. While everyday code rarely needs them, they are indispensable when you work with large data streams, complex iteration logic, or stateful step-by-step processes.