JavaScriptGenerators

Generators

A generator is a function that can pause and resume. You write it like ordinary code, but you use yield to hand a value back to the caller and stop until the caller asks for more. Under the hood, calling a generator produces an iterator — so anything that consumes iterators (for...of, spread, destructuring) consumes generators for free.

function* and yield

A generator function is declared with function*. Calling it doesn't run the body — it returns a generator object, which is both an iterator and an iterable.

JS
function* abc() {
  yield "a";
  yield "b";
  yield "c";
}

const g = abc();
console.log(g.next());   // { value: 'a', done: false }
console.log(g.next());   // { value: 'b', done: false }
console.log(g.next());   // { value: 'c', done: false }
console.log(g.next());   // { value: undefined, done: true }
{ value: 'a', done: false }
{ value: 'b', done: false }
{ value: 'c', done: false }
{ value: undefined, done: true }

Each yield is a pause point. The function suspends after yielding and only continues when next() is called again. When the function returns (or runs off the end), done becomes true.

Using a generator with for...of

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

for (const n of range(0, 5)) console.log(n);
console.log([...range(0, 10, 2)]);
0
1
2
3
4
[ 0, 2, 4, 6, 8 ]

Compare to the manual iterator on the Iterators page — this version is about a third of the code, and you can read it top to bottom.

Lazy evaluation

The body of a generator only runs as far as the next yield. That makes generators perfect for sequences where computing every value is expensive or impossible.

Infinite sequence of natural numbers

JS
function* naturals() {
  let i = 1;
  while (true) yield i++;
}

const g = naturals();
console.log(g.next().value);   // 1
console.log(g.next().value);   // 2
console.log(g.next().value);   // 3

Yes, the loop body says while (true) — but it only runs one tick per next(), so the function never actually loops forever unless you keep asking.

Taking the first n values

A common pattern: combine an infinite generator with a "take" helper.

JS
function* take(iterable, n) {
  let i = 0;
  for (const value of iterable) {
    if (i++ >= n) return;
    yield value;
  }
}

console.log([...take(naturals(), 5)]);   // [ 1, 2, 3, 4, 5 ]

return inside a generator marks it done — anything after won't run.

yield*: delegate to another iterable

Inside a generator, yield* yields all the values of another iterable in order. It's the easy way to compose generators or flatten nested sequences.

JS
function* letters() { yield "a"; yield "b"; yield "c"; }
function* digits()  { yield 1;   yield 2;   yield 3; }

function* both() {
  yield* letters();
  yield* digits();
}

console.log([...both()]);
[ 'a', 'b', 'c', 1, 2, 3 ]
Two-way comms with next(value)

This is where generators get unusual. yield is an expression — and the value passed to the next next(value) call becomes the result of the paused yield. That gives you two-way communication between caller and generator.

JS
function* dialogue() {
  const name = yield "What is your name?";
  const lang = yield `Hi ${name}. Favourite language?`;
  return `${name} likes ${lang}.`;
}

const d = dialogue();
console.log(d.next().value);          // "What is your name?"
console.log(d.next("Ada").value);     // "Hi Ada. Favourite language?"
console.log(d.next("Lisp").value);    // "Ada likes Lisp."

The first next() doesn't pass anything — there's no yield waiting yet. Each subsequent call's argument feeds the previously paused yield.

Why this is interesting
Two-way generators are how some libraries implemented `async/await` before it was native — the generator yields a promise, the runner awaits it, then `next(resolvedValue)` resumes the generator. Same trick powers Redux Saga.
throw() and return()

Generators expose two more methods besides next:

  • gen.throw(err) — resume the generator by throwing err at the paused yield. Useful for surfacing errors from a runner.

  • gen.return(value) — finish the generator immediately, as if a return value had run at the current pause point. Cleanup paths in for...of use this.

JS
function* safe() {
  try {
    yield 1;
    yield 2;
  } catch (err) {
    console.log("caught:", err.message);
  }
}

const g = safe();
console.log(g.next().value);          // 1
console.log(g.throw(new Error("nope")));
// caught: nope
// { value: undefined, done: true }
Practical recipes

A few patterns that come up often:

ID generator

JS
function* idGenerator(prefix = "id") {
  let i = 0;
  while (true) yield `${prefix}-${++i}`;
}

const ids = idGenerator("user");
console.log(ids.next().value);   // user-1
console.log(ids.next().value);   // user-2

Walking a tree lazily

JS
function* walk(node) {
  yield node;
  for (const child of node.children ?? []) {
    yield* walk(child);
  }
}

for (const node of walk(root)) {
  if (node.id === "target") return node;   // stops the generator early
}
One sentence to remember
A generator is a pausable function — `yield` hands values to the caller, `next(value)` resumes with a result, and the whole thing is automatically an iterator.