JavaScript Generators Tutorial: yield, next(), and Lazy Iteration

Learn how function*, yield, next(), and yield* work through runnable traces. Build lazy ranges, bound infinite sequences, send values inward, and close generators safely.

KnowledgeGate Team

Exam prep & CS education

Updated 2 Sep 20265 min read

An ordinary function runs from entry to return, but some tasks need to produce one value, pause, and continue later without rebuilding their state. JavaScript generators provide that control. Generators expose each next() result and can also produce a lazy numeric range.

What a JavaScript generator returns

function* defines a generator function. Calling it returns a generator object without running the body to completion. Each yield pauses the body, and next() resumes it. The returned iterator result has two fields: value is the yielded or returned value, while done says whether the generator has finished.

function* ticketCounter() {
  yield "A101";
  yield "A102";
  return "closed";
}

const counter = ticketCounter();
counter.next(); // { value: "A101", done: false }
counter.next(); // { value: "A102", done: false }
counter.next(); // { value: "closed", done: true }
counter.next(); // { value: undefined, done: true }

The first call starts execution and pauses at the first yield. The third call reaches return "closed", so that value appears in the manual result that completes the generator. However, for...of and spread collect yielded values only. Therefore, [...ticketCounter()] is exactly ["A101", "A102"].

Execution trace of ticketCounter() where each next() advances past one yield and returns its { value, done } result until done is true.

Build a lazy stepped range

This generator yields a finite arithmetic sequence. The guard matters because step = 0 would prevent the loop from advancing and create an endless sequence.

function* range(start, end, step = 1) {
  if (!Number.isFinite(step) || step <= 0) {
    throw new RangeError("step must be a positive number");
  }

  for (let n = start; n <= end; n += step) {
    yield n;
  }
}

const values = [...range(3, 11, 2)];
const total = values.reduce((sum, n) => sum + n, 0);

Start at 3 and add 2 after every resume: 3, 5, 7, 9, 11. The next call after 11 returns { value: undefined, done: true }. Thus values is [3, 5, 7, 9, 11], and the sum is 3 + 5 + 7 + 9 + 11 = 35. The generator calculates one n per resume instead of constructing the whole array inside range.

Each call creates fresh state. If a and b are both range(3, 11, 2), advancing a twice gives 3 and 5, while the first b.next() still gives 3.

Consume a finite slice safely

A manual loop exposes each { value, done } result. for...of visits 3, 5, 7, 9, 11; spread creates [3, 5, 7, 9, 11]; and const [first, second] = range(3, 11, 2) assigns first = 3 and second = 5.

An unbounded generator needs a bounded consumer:

function* ids(start = 100) {
  let id = start;
  while (true) yield `ID-${id++}`;
}

function take(iterator, count) {
  const output = [];
  for (let i = 0; i < count; i += 1) {
    const { value, done } = iterator.next();
    if (done) break;
    output.push(value);
  }
  return output;
}

take(ids(100), 3); // ["ID-100", "ID-101", "ID-102"]

take(..., 3) stops after three values. By contrast, [...ids(100)] never reaches done: true, so it does not finish and must not be used.

Compose sequences with yield*

yield* delegates to another iterable and forwards its values.

function* featuredScores() {
  yield* range(1, 5, 2);
  yield 99;
}

[...featuredScores()]; // [1, 3, 5, 99]

Here, yield* range(1, 5, 2) forwards 1, 3, and 5. Writing yield range(1, 5, 2) would yield one generator object instead. The same rule works with built-in iterables: yield* ["home", "courses"] emits the two strings separately. Use yield* when one generator should expose another iterable's values as part of its own sequence.

Send values back into a paused generator

Generators can receive data as well as produce it:

function* billCalculator() {
  const quantity = yield "quantity?";
  const unitPrice = yield "unit price?";
  return quantity * unitPrice;
}

const bill = billCalculator();
bill.next("ignored"); // { value: "quantity?", done: false }
bill.next(4);         // { value: "unit price?", done: false }
bill.next(125);       // { value: 500, done: true }

The 4 becomes the result of the suspended yield "quantity?", and 125 becomes the result of yield "unit price?". The first next() argument is ignored because execution has not yet paused at a yield expression. Prompts travel out; later next(value) arguments travel back in. The final calculation is 4 * 125 = 500.

Message-flow diagram of billCalculator() showing prompts yielded out to the caller and next() values sent back in to compute 4 * 125 = 500.

Stop early and run cleanup

Calling return() asks a generator to finish immediately. A finally block still runs, which makes it suitable for cleanup.

function* inventory() {
  try {
    yield "keyboard";
    yield "mouse";
  } finally {
    console.log("inventory closed");
  }
}

const items = inventory();
items.next();            // { value: "keyboard", done: false }
items.return("stopped"); // logs "inventory closed"
                         // returns { value: "stopped", done: true }
items.next();            // { value: undefined, done: true }

generator.throw(error) resumes at the paused yield by throwing there, so treat it as recoverable only when the generator has a matching try...catch.

Common errors and generator practice tasks

Mistake

What happens

Fix

Write yield inside function counter() {}

Syntax error

Add * to make function* counter() {}

Expect ticketCounter() to equal "A101"

You get a generator object

Call next()

Pass data in the first next(value)

The value is lost

Prime the generator before sending

Expect return "closed" in spread

The array omits it

Inspect the terminal manual result

Call next() on an exhausted counter

It stays finished

Create a fresh generator object

Spread ids(100)

It never finishes

Consume a bounded count

For practice, first implement an unbounded Fibonacci generator. With state a = 0, b = 1, the first yield is 0, then update to (1, 1); the second yield is 1, then update to (1, 2); the third yield is 1, then update to (2, 3). Your check is take(fibonacci(), 7), which must return [0, 1, 1, 2, 3, 5, 8].

Next, implement rangeDown(10, 1, 3). Its state variable pauses at 10, 7, and 4 for the first three yields, then reaches 1. Spread must return [10, 7, 4, 1].

Useful assessment tasks include predicting a next() trace, repairing a missing function*, writing a bounded lazy sequence, and explaining why spread hangs. Continue with String Handling in Java for another language-state deep dive, or practise more step-by-step code with SQL Queries for Placement Interviews.

JavaScript generators in the short version

  • function* creates resumable logic.

  • yield produces a value and preserves local state.

  • next() returns { value, done } and can send a value into a paused yield.

  • yield* delegates to another iterable.

The range 3, 5, 7, 9, 11 shows lazy production, while 4 * 125 = 500 shows two-way communication. Your main next step is the Complete JavaScript Course. Move to the React and Redux Course when you are ready to apply language mechanics in application development, or browse the Coding & Skill Development Courses for a wider path.