Skip to content
JavaScript js collections 5 min read

Generators

Generators are special functions that can pause execution and resume later, producing a sequence of values one at a time instead of computing them all up front. Declared with function* and powered by the yield keyword, they make it trivial to build custom iterators, model infinite streams, and even pass values back into a running function. They are the cleanest way in JavaScript to express lazy, on-demand computation.

Generator functions and yield

A generator function is written with an asterisk after the function keyword. Calling it does not run the body — instead it returns a generator object, which is both an iterator and an iterable. Execution only advances when you call .next(), and it pauses at each yield, handing the yielded value back to the caller.

function* greetings() {
  yield "Hello";
  yield "Hola";
  yield "Bonjour";
}

const gen = greetings();
console.log(gen.next()); // { value: 'Hello', done: false }
console.log(gen.next()); // { value: 'Hola', done: false }
console.log(gen.next()); // { value: 'Bonjour', done: false }
console.log(gen.next()); // { value: undefined, done: true }

Output:

{ value: 'Hello', done: false }
{ value: 'Hola', done: false }
{ value: 'Bonjour', done: false }
{ value: undefined, done: true }

Each .next() call resumes the function until the next yield, then freezes it again. When the function returns (or runs off the end), done becomes true. Because the generator object is iterable, you can drive it with for...of, the spread operator, or destructuring without touching .next() directly.

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

console.log([...range(1, 5)]);     // spread
for (const n of range(1, 5)) {     // for...of
  console.log(n);
}

Output:

[ 1, 2, 3, 4 ]
1
2
3
4

The return value of a generator is exposed as the value when done is true, but for...of and spread ignore it. Use yield for values you want consumers to see in a loop.

Lazy and infinite sequences

Because nothing is computed until requested, generators can describe sequences that are conceptually infinite. The body only runs as far as the consumer pulls, so an endless while (true) loop is perfectly safe — provided you stop asking for values.

function* naturalNumbers() {
  let n = 1;
  while (true) {
    yield n++;
  }
}

const numbers = naturalNumbers();
console.log(numbers.next().value); // 1
console.log(numbers.next().value); // 2
console.log(numbers.next().value); // 3

Output:

1
2
3

This laziness is the real power: you can compose pipelines that never materialize a full collection in memory. Here is a small take helper that pulls a finite slice from any iterable, including an infinite one.

function* fibonacci() {
  let [a, b] = [0, 1];
  while (true) {
    yield a;
    [a, b] = [b, a + b];
  }
}

function take(n, iterable) {
  const result = [];
  for (const value of iterable) {
    if (result.length >= n) break;
    result.push(value);
  }
  return result;
}

console.log(take(8, fibonacci()));

Output:

[ 0, 1, 1, 2, 3, 5, 8, 13 ]

Two-way communication with next(value)

yield is an expression, not just a statement — it evaluates to whatever is passed into the next call to .next(value). This turns a generator into a two-way channel: it can both emit values and receive them, resuming with the supplied input as the result of the paused yield.

function* conversation() {
  const name = yield "What is your name?";
  const lang = yield `Hi ${name}, what language do you code in?`;
  return `${name} loves ${lang}.`;
}

const chat = conversation();
console.log(chat.next().value);        // first yield runs, no value consumed
console.log(chat.next("Ada").value);   // "Ada" becomes the result of yield #1
console.log(chat.next("JavaScript").value);

Output:

What is your name?
Hi Ada, what language do you code in?
Ada loves JavaScript.

Note that the value passed to the first .next() is discarded — there is no paused yield waiting to receive it yet. You can also inject control flow: .throw(err) raises an exception at the paused yield, and .return(value) finishes the generator early as if a return ran at that point.

MethodEffect at the paused yield
.next(value)Resumes; yield evaluates to value
.throw(error)Throws error inside the generator (catchable)
.return(value)Ends the generator; returns { value, done: true }

Delegation with yield*

When one generator needs to yield every value from another iterable, yield* delegates to it. It forwards each value transparently and evaluates to the delegated generator’s return value, making composition clean.

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

function* numbers() {
  yield 1;
  yield 2;
}

function* combined() {
  yield* letters();
  yield* numbers();
  yield* [true, false]; // any iterable works
}

console.log([...combined()]);

Output:

[ 'a', 'b', 1, 2, true, false ]

Bridge to async generators

Standard generators are synchronous — each value is available immediately. For values that arrive over time (network chunks, file streams, paginated APIs), ES2018 introduced async generators, declared with async function*. They can await inside the body and yield results, and you consume them with for await...of.

async function* fetchPages(urls) {
  for (const url of urls) {
    const res = await fetch(url);
    yield res.json();
  }
}

async function run() {
  for await (const page of fetchPages(["/api/1", "/api/2"])) {
    console.log(page);
  }
}

Async generators produce an async iterator, where each .next() returns a promise. They are ideal for streaming data lazily without buffering an entire result set.

Best Practices

  • Reach for generators when modeling sequences, custom iteration, or lazy pipelines — not as a substitute for plain arrays of known, small size.
  • Always provide an exit condition (or use take/break) when consuming infinite generators to avoid hanging loops.
  • Remember that a generator object is single-use: once exhausted, create a fresh one to iterate again.
  • Use yield* instead of manually looping and re-yielding when composing iterables.
  • Prefer for...of and spread over manual .next() calls for cleaner, error-resistant consumption.
  • Reserve async generators (async function* with for await...of) for asynchronous data streams; keep synchronous logic in regular generators.
Last updated June 1, 2026
Was this helpful?