Chapter 6.7☕ 20 min read

Generators: Pause, Resume & Lazy Evaluation

Yield se ruko, next se jao — aur V8 stack frame heap pe rakh ke slow ho jao. Lekin memory king ho!

01Generator Basics: function* and yield

A generator function is declared with function* (the asterisk is part of the syntax). Calling it does not execute the body — instead, it returns a Generator object that controls execution.

Key insight: Calling a generator function does NOT run its body. It returns an iterator object. The body only runs when you call .next() on that object. This is fundamentally different from regular functions.

yield pauses the function and sends a value back to the caller. .next() resumes from the last yield. Each .next() returns { value, done }.

// Basic Generator
function* greet() {
  console.log("Started");
  yield "Hello";
  console.log("Resumed");
  yield "World";
  console.log("Finished");
}

// Calling greet() does NOT run the body!
const gen = greet();

console.log(gen.next());
// Logs: "Started"
// Returns: { value: "Hello", done: false }

console.log(gen.next());
// Logs: "Resumed"
// Returns: { value: "World", done: false }

console.log(gen.next());
// Logs: "Finished"
// Returns: { value: undefined, done: true }

// Generators are iterable
for (const word of greet()) {
  console.log(word); // "Hello", "World"
}

Generator objects are both Iterator and Iterable — they implement [Symbol.iterator] returning this. That's why for...of works with them directly.

02Two-Way Communication: yield as Expression

yield doesn't just send values OUT — it can receive values IN via .next(value). The value passed to .next() becomes the result of the yield expression on the generator's side.

First .next() always ignores its argument — nothing has been yielded yet to receive it. This trips up many developers.

function* dialog() {
  const name = yield "What is your name?";
  const food = yield "Hello, " + name + "! Favorite food?";
  return name + " loves " + food;
}

const chat = dialog();

// First next() starts the generator, argument is ignored
console.log(chat.next());
// { value: "What is your name?", done: false }

// Pass "Sai" back in — becomes result of first yield
console.log(chat.next("Sai"));
// { value: "Hello, Sai! Favorite food?", done: false }

// Pass "Biryani" back in
console.log(chat.next("Biryani"));
// { value: "Sai loves Biryani", done: true }
Under the hood: When a generator yields, V8 copies its entire execution context (local variables, instruction pointer) to a heap-allocated "generator frame". When .next() is called, this context is copied back to the stack. This heap allocation on every yield is why generators are 3-5x slower than regular function calls.
03Lazy Evaluation: Infinite Sequences

Generators don't compute values until asked. This is lazy evaluation — the opposite of eager computation where everything is calculated upfront.

You can create infinite sequences without infinite loops or memory issues. The generator only computes the next value when .next() is called.

// Infinite sequence of natural numbers
function* naturalNumbers() {
  let n = 1;
  while (true) { // Infinite loop — but safe!
    yield n++;
  }
}

// Consuming an infinite generator
const numbers = naturalNumbers();
console.log(numbers.next().value); // 1
console.log(numbers.next().value); // 2

// Taking first N items
function take(gen, n) {
  const result = [];
  for (let i = 0; i < n; i++) {
    const item = gen.next();
    if (item.done) break;
    result.push(item.value);
  }
  return result;
}

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

// Lazy Fibonacci
function* fibonacci() {
  let a = 0, b = 1;
  while (true) {
    yield a;
    [a, b] = [b, a + b];
  }
}
console.log(take(fibonacci(), 10)); // [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
Why this matters: Imagine processing 1 crore records from a database. An array would need all 1 crore in memory at once. A generator holds only ONE record at a time. The memory difference is enormous — O(n) vs O(1).
04yield* Delegation: Composing Generators

yield* delegates to another iterable or generator. It yields all values from the delegated iterable, then resumes the outer generator.

The return value of yield* is the return value of the inner generator (not a yielded value).

function* inner() {
  yield "a";
  yield "b";
  return "inner-done"; // Return value (not yielded)
}

function* outer() {
  yield 1;
  // Delegates all yields to inner()
  const result = yield* inner();
  console.log("Inner returned:", result); // "inner-done"
  yield 2;
}

for (const val of outer()) {
  console.log(val);
}
// Logs: 1, "a", "b", "Inner returned: inner-done", 2
// Note: "inner-done" is NOT yielded, it is the return value of yield*
yield* is NOT the same as yield: yield inner() would yield the generator object itself. yield* inner() iterates through the entire inner generator, yielding each value individually. Think of yield* as "flatten and delegate".
05Generator Methods: .return() and .throw()

Beyond .next(), generators have two powerful control methods:

gen.return(value): Forces the generator to finish immediately. Returns { value, done: true }. Runs finally blocks.

gen.throw(error): Throws an error inside the generator at the current yield point. If caught, the generator can continue.

function* resilient() {
  try {
    yield 1;
    yield 2;
    yield 3;
  } finally {
    console.log("Cleanup!"); // Always runs on return() or throw()
  }
}

const gen = resilient();
console.log(gen.next()); // { value: 1, done: false }

// Force finish
console.log(gen.return("stopped"));
// Logs: "Cleanup!"
// Returns: { value: "stopped", done: true }

// Throwing into a generator
function* catcher() {
  try {
    yield 1;
  } catch (err) {
    console.log("Caught:", err.message);
    yield "recovered"; // Can continue yielding!
  }
  yield 2;
}

const c = catcher();
c.next(); // Start
c.throw(new Error("Oops!")); // Throws at yield point
// Logs: "Caught: Oops!"
// Returns: { value: "recovered", done: false }
📋 Historical note:
• Generators were the original way to handle async before async/await
• Libraries like "co" and "redux-saga" used them heavily
• While async/await is now standard for promises, generators are still the best tool for lazy data streams, infinite sequences, and complex state machines

Lo kar liya — Key Points:

  • ✅ Generators (function*) return Generator objects that are both iterable and iterator
  • ✅ yield pauses the function; .next() resumes it and passes values in/out via yield expressions
  • ✅ Generators enable lazy evaluation — infinite sequences consume zero memory until .next() is called
  • ✅ yield* delegates to another iterable/generator and captures its return value
  • ✅ .return(value) forces the generator to finish and runs finally blocks; .throw(error) injects an error at the yield
  • ✅ V8 copies the generator's execution context to the heap on yield, making generators 3-5x slower than regular functions
Course Search
Search across all chapters & stages
📖

Search the course

Type any topic — branching, stash, rebase, hooks — and jump straight to that chapter.

merge branchesgit stashundo commitrebase