JavaScript Generators

A generator is a special kind of function that can pause its own execution partway through and resume later, exactly where it left off. Unlike a normal function, which runs start-to-finish and hands back one value, a generator produces a sequence of values over time, one at a time, on demand. Generators are the mechanism behind custom iterators and lazy sequences in JavaScript, and understanding them makes constructs like for...of, the spread operator, and destructuring over iterables click into place.

Overview: How Generators Work

A generator is declared with the function* syntax (an asterisk after function). Calling a generator function does not run its body immediately. Instead, it returns a special generator object that acts as both an iterator and an iterable. Nothing inside the function runs until you call .next() on that object.

Each call to .next() resumes execution from wherever the function last paused, and runs until it hits a yield expression. At that point, execution freezes again: the value after yield is packaged into an object of the shape { value, done } and handed back to the caller, and the function’s entire state — local variables, the current position in loops, even pending try/finally blocks — is preserved in memory untouched. The next call to .next() picks up from that exact spot.

This is fundamentally different from a normal function call, which has no memory of previous invocations. A generator’s execution context stays alive on a kind of suspended stack between calls, which is why generators can model infinite sequences, lazily-computed data, and stateful iteration without you needing to manage index variables or closures by hand.

When the function body finally runs to completion (falls off the end, or hits an explicit return), the generator object’s done flag becomes true and value holds the returned value (or undefined if there was no explicit return). Every subsequent call to .next() after that simply returns { value: undefined, done: true } forever — a finished generator cannot be restarted.

Syntax

The general shape of a generator looks like this:

function* rangeGenerator(start, end) {
  for (let i = start; i <= end; i++) {
    yield i;
  }
  return 'done';
}

const gen = rangeGenerator(1, 3);
console.log(gen.next());
console.log(gen.next());
console.log(gen.next());
console.log(gen.next());

Output:

{ value: 1, done: false }
{ value: 2, done: false }
{ value: 3, done: false }
{ value: 'done', done: true }

Key parts of the syntax:

  • function* — marks the function as a generator. The asterisk can also go before the name (function *rangeGenerator) or, inside objects and classes, as a method shorthand: *methodName() { ... }. There is no arrow-function equivalent — generators must be written with the function* form or the method shorthand.
  • yield expr — pauses the function and emits expr as the value of the returned object. Execution halts right after this line until .next() is called again.
  • return expr — ends the generator early (or naturally at the end of the body) and sets done: true with value equal to expr.
  • gen.next(arg) — resumes the generator. The optional arg becomes the result of the yield expression that was paused, enabling two-way communication (see Example 2).
Method Purpose
gen.next(value) Resumes execution, optionally injecting value as the result of the paused yield.
gen.return(value) Forces the generator to finish immediately as if it had return value, running any pending finally blocks.
gen.throw(error) Resumes the generator by throwing error at the paused yield point, which can be caught inside the generator with try/catch.
gen[Symbol.iterator]() Returns the generator itself, which is why generators work directly with for...of and the spread operator.

Examples

Example 1: Pausing and resuming execution

function* numberGenerator() {
  console.log('Start');
  yield 1;
  console.log('Between 1 and 2');
  yield 2;
  console.log('Between 2 and 3');
  yield 3;
  console.log('End');
}

const gen = numberGenerator();
console.log(gen.next());
console.log(gen.next());
console.log(gen.next());
console.log(gen.next());

Output:

Start
{ value: 1, done: false }
Between 1 and 2
{ value: 2, done: false }
Between 2 and 3
{ value: 3, done: false }
End
{ value: undefined, done: true }

Notice that nothing prints until the first .next() call — the body doesn’t start running when numberGenerator() is invoked, only when it’s iterated. Each subsequent call resumes exactly after the previous yield, runs any code in between, and stops at the next one. The final call runs past the last yield, prints 'End', and completes with done: true.

Example 2: Two-way communication with next()

function* multiplier() {
  const a = yield 'Enter first number';
  const b = yield 'Enter second number';
  return a * b;
}

const gen = multiplier();
console.log(gen.next());
console.log(gen.next(5));
console.log(gen.next(10));

Output:

{ value: 'Enter first number', done: false }
{ value: 'Enter second number', done: false }
{ value: 50, done: true }

This example shows that yield is a two-way channel, not just a one-way output. The first .next() only starts the generator and has no value to deliver (there’s no paused yield yet), so any argument passed to it is discarded. The second call, gen.next(5), supplies 5 as the result of the first yield expression, which gets assigned to a. Similarly, gen.next(10) supplies b. The generator then returns a * b, which is 50.

Example 3: Infinite sequences and delegation with yield*

function* idGenerator() {
  let id = 1;
  while (true) {
    yield id++;
  }
}

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

function* combined() {
  yield* colors();
  yield 'extra';
}

const ids = idGenerator();
console.log(ids.next().value);
console.log(ids.next().value);
console.log(ids.next().value);

for (const color of combined()) {
  console.log(color);
}

Output:

1
2
3
red
green
blue
extra

idGenerator has a while (true) loop but never hangs, because a generator body only advances when something calls .next() — it’s safe to represent infinite sequences this way, since consumers control the pace. yield* delegates iteration to another iterable, pulling all of its values through as if they were written inline; that’s how combined() yields 'red', 'green', and 'blue' from colors() before yielding its own 'extra'. Because generator objects implement the iterable protocol, a plain for...of loop can consume combined() directly without calling .next() manually.

Under the Hood: The Iterator Protocol and Cleanup

JavaScript’s iteration machinery is built on two small protocols. An object is an iterator if it has a .next() method that returns { value, done }. An object is iterable if it has a method at the well-known key Symbol.iterator that returns an iterator. A generator object satisfies both at once: it has its own .next(), and its Symbol.iterator method simply returns itself. That dual nature is exactly why for...of, the spread operator ([...gen]), Array.from(gen), and destructuring all accept a generator object without any adapter code.

Internally, when you call a generator function, the engine allocates a suspended execution context instead of running the function body on the normal call stack to completion. Each yield unwinds that context back to the caller while preserving local variables, the instruction pointer, and any active try/catch/finally blocks. Calling .next() again re-enters that exact context. This is also why calling .return(value) on a generator that is paused inside a try block still runs the corresponding finally — the engine treats early termination like an exception unwinding through that stack frame, cleanup and all.

function* resourceGenerator() {
  try {
    yield 'Resource acquired';
    yield 'Still using resource';
  } finally {
    console.log('Cleaning up resource');
  }
}

const gen = resourceGenerator();
console.log(gen.next());
console.log(gen.return('Stopped early'));

Output:

{ value: 'Resource acquired', done: false }
Cleaning up resource
{ value: 'Stopped early', done: true }

Here, gen.return('Stopped early') forces the generator to finish immediately, but because it was paused inside a try block, the engine still runs the finally clause before handing back the final { value, done: true } object. The related method gen.throw(error) works similarly, but instead injects an exception at the paused yield, which the generator can catch with an ordinary try/catch around the yield.

Common Mistakes

Mistake 1: Expecting the call to run the body immediately. Beginners often assume calling a generator function behaves like a regular function that returns a completed result.

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

const result = countUp();
console.log(result);

Output:

Object [Generator] {}

Nothing has executed yet — result is just the generator object, not an array of values. To actually collect the yielded values, iterate it, for example with the spread operator:

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

const values = [...countUp()];
console.log(values);

Output:

[ 1, 2, 3 ]

Mistake 2: Reusing an exhausted generator. A generator object is single-use; once it reaches done: true, it cannot be rewound or restarted.

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

const gen = letters();
console.log([...gen]);
console.log([...gen]);

Output:

[ 'a', 'b' ]
[]

The second spread produces an empty array because the generator was already fully drained by the first one. If you need to iterate the same sequence twice, call the generator function again to get a fresh generator object, rather than reusing the old one. It’s also worth knowing that function* cannot be written as an arrow function — there is no arrow-generator syntax in JavaScript, so const gen = *() => {} is invalid and throws a SyntaxError; generators must use function* or the *method() shorthand.

Best Practices

  • Use generators when you need lazy, on-demand sequences (infinite counters, paginated data, tree/graph traversal) rather than building a full array up front.
  • Prefer yield* to delegate to nested generators or other iterables instead of manually looping and re-yielding each value.
  • Add a Symbol.iterator generator method (*[Symbol.iterator]() { ... }) to custom classes to make your own data structures work with for...of and the spread operator.
  • Wrap resource-sensitive code in try/finally inside a generator so cleanup still runs if a consumer stops early with .return() or breaks out of a for...of loop.
  • Don’t rely on a generator object being reusable — treat it as a one-shot stream and re-invoke the generator function whenever you need to iterate again.
  • Reach for async function* (async generators) when the values you’re yielding need to be awaited, rather than mixing plain generators with manual promise handling.

Practice Exercises

  • Write a generator function fibonacci() that yields an endless sequence of Fibonacci numbers (0, 1, 1, 2, 3, 5, …). Use a loop and .next() to print only the first 8 values.
  • Write a generator takeFirst(iterable, n) is not required — instead, write a generator function batch(array, size) that yields successive chunks (sub-arrays) of array, each of length size (the last chunk may be shorter). For example, batch([1,2,3,4,5], 2) should yield [1,2], then [3,4], then [5].
  • Write a generator flatten(nested) that takes an array which may contain other arrays nested to any depth, and yields every non-array value in order, flattened. Test it with [1, [2, 3, [4, 5]], 6] and confirm it yields 1, 2, 3, 4, 5, 6.

Summary

  • A generator is declared with function* and, when called, returns a generator object instead of running immediately.
  • yield pauses execution and emits a value; the next .next() call resumes right after that point with the function’s state fully preserved.
  • gen.next(value) can pass data back into a paused yield expression, enabling two-way communication between caller and generator.
  • Generator objects are both iterators and iterables, so they work directly with for...of, the spread operator, and destructuring.
  • yield* delegates to another iterable, and gen.return() / gen.throw() allow controlled early termination or error injection, with pending finally blocks still running.
  • A generator object is single-use: once exhausted (done: true), it must be replaced by calling the generator function again, not restarted.