Ad – 728Γ—90
πŸš€ Advanced JS

JavaScript Generators and Iterators – Lazy Evaluation

Generator functions (function*) are special functions that can pause execution mid-body, yield a value to the caller, and resume from where they left off when the caller asks for the next value. This "lazy evaluation" pattern is perfect for infinite sequences, data streaming, custom iteration, and cooperative concurrency. This lesson covers everything from basic yield to the iterator protocol, yield* delegation, and practical real-world use cases.

⏱️ 22 min read 🎯 Advanced πŸ“… Updated 2026

Generator Function Syntax

A generator function is declared with function* (asterisk). Calling it does not execute the body β€” it returns a generator object. The body only runs when you call .next() on the generator object.

JavaScript
function* simpleGenerator() {
  console.log('Body: before first yield');
  yield 1;
  console.log('Body: after first yield');
  yield 2;
  console.log('Body: after second yield');
  yield 3;
  console.log('Body: done');
}

const gen = simpleGenerator();
console.log('Generator created β€” body not running yet');

let result;

result = gen.next();
console.log('next() returned:', result);    // { value: 1, done: false }

result = gen.next();
console.log('next() returned:', result);    // { value: 2, done: false }

result = gen.next();
console.log('next() returned:', result);    // { value: 3, done: false }

result = gen.next();
console.log('next() returned:', result);    // { value: undefined, done: true }
β–Ά Output
Generator created β€” body not running yet
Body: before first yield
next() returned: { value: 1, done: false }
Body: after first yield
next() returned: { value: 2, done: false }
Body: after second yield
next() returned: { value: 3, done: false }
Body: done
next() returned: { value: undefined, done: true }

Infinite Sequence Generator

Generators can represent infinite sequences because values are computed lazily β€” only when requested. A while(true) loop that yields is perfectly safe inside a generator.

JavaScript
// Infinite integer counter
function* counter(start = 0, step = 1) {
  let current = start;
  while (true) {
    yield current;
    current += step;
  }
}

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

// Helper: take first N values from any generator
function take(gen, n) {
  const result = [];
  for (const value of gen) {
    result.push(value);
    if (result.length >= n) break;
  }
  return result;
}

console.log(take(counter(1, 2), 6));    // [1, 3, 5, 7, 9, 11]
console.log(take(fibonacci(), 10));     // [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
console.log(take(counter(100, -10), 5)); // [100, 90, 80, 70, 60]
β–Ά Output
[1, 3, 5, 7, 9, 11]
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
[100, 90, 80, 70, 60]

Passing Values Into Generators via next(value)

You can pass a value into a running generator via gen.next(value). The passed value becomes the result of the yield expression inside the generator. This enables two-way communication.

JavaScript
function* dialog() {
  const name    = yield 'What is your name?';
  const hobbies = yield `Nice to meet you, ${name}! What are your hobbies?`;
  yield `Cool, ${name} β€” I enjoy ${hobbies} too!`;
}

const conv = dialog();

let q = conv.next();         // start: value = first question
console.log(q.value);        // 'What is your name?'

q = conv.next('Alice');      // send 'Alice' as the result of first yield
console.log(q.value);        // 'Nice to meet you, Alice! What are your hobbies?'

q = conv.next('coding');     // send 'coding' as result of second yield
console.log(q.value);        // 'Cool, Alice β€” I enjoy coding too!'
β–Ά Output
What is your name?
Nice to meet you, Alice! What are your hobbies?
Cool, Alice β€” I enjoy coding too!

The Symbol.iterator Protocol

An object is iterable if it has a [Symbol.iterator]() method that returns an iterator. An iterator is an object with a .next() method that returns { value, done }. Generators automatically implement both protocols.

JavaScript
// Custom iterable class using a generator
class Range {
  constructor(start, end, step = 1) {
    this.start = start;
    this.end   = end;
    this.step  = step;
  }

  // The Symbol.iterator method makes instances iterable
  *[Symbol.iterator]() {
    for (let i = this.start; i <= this.end; i += this.step) {
      yield i;
    }
  }

  // Convenience: collect to array
  toArray() { return [...this]; }
}

const r = new Range(1, 10, 2);

// Works with for...of
for (const n of r) {
  process.stdout.write(n + ' ');
}
console.log();

// Works with spread
console.log([...new Range(0, 6, 3)]);

// Works with destructuring
const [a, b, c] = new Range(10, 50, 15);
console.log(a, b, c);
β–Ά Output
1 3 5 7 9
[0, 3, 6]
10 25 40

yield* Delegation

yield* delegates to another iterable (another generator, array, string, etc.). It's like yielding every item from the inner iterable one by one.

JavaScript
function* inner() {
  yield 'a';
  yield 'b';
}

function* outer() {
  yield 1;
  yield* inner();          // delegate to inner generator
  yield* [10, 20, 30];    // delegate to array
  yield* 'hi';            // delegate to string (character by character)
  yield 2;
}

console.log([...outer()]);
// [1, 'a', 'b', 10, 20, 30, 'h', 'i', 2]
β–Ά Output
[1, 'a', 'b', 10, 20, 30, 'h', 'i', 2]

Practical Use Cases

JavaScript
// Use-case 1: Unique ID factory
function* idFactory(prefix = 'id') {
  let n = 1;
  while (true) {
    yield `${prefix}-${String(n++).padStart(4, '0')}`;
  }
}

const userIds = idFactory('usr');
console.log(userIds.next().value);  // usr-0001
console.log(userIds.next().value);  // usr-0002

// Use-case 2: Paginated API fetching
async function* paginatedFetch(baseUrl) {
  let page = 1;
  while (true) {
    const res  = await fetch(`${baseUrl}?page=${page}&limit=10`);
    const data = await res.json();
    if (data.length === 0) break;
    yield data;           // yield entire page at once
    page++;
  }
}

// Consumer
async function fetchAllUsers() {
  const allUsers = [];
  for await (const page of paginatedFetch('/api/users')) {
    allUsers.push(...page);
    console.log(`Fetched ${allUsers.length} users so far`);
  }
  return allUsers;
}

// Use-case 3: Tree traversal
function* flattenTree(node) {
  yield node.value;
  if (node.children) {
    for (const child of node.children) {
      yield* flattenTree(child);  // recursive delegation
    }
  }
}

const tree = { value: 1, children: [
  { value: 2, children: [{ value: 4 }, { value: 5 }] },
  { value: 3, children: [{ value: 6 }] }
]};

console.log([...flattenTree(tree)]);  // [1, 2, 4, 5, 3, 6]
β–Ά Output
usr-0001
usr-0002
[1, 2, 4, 5, 3, 6]
Use CaseWhy Generators HelpAlternative
Infinite sequencesLazy β€” compute only when askedCannot do with plain arrays
ID factoriesStateful counter, trivially simpleClosure variable
PaginationAsync generators hide fetching logicRecursive async functions
Tree traversalyield* enables elegant recursionManual stack with array
Custom iterationSymbol.iterator + generator = minimal codeManual iterator object
πŸ’‘
Async generators (for await...of)

Prefix a generator function with both async and *: async function*. It can both await Promises and yield values. Consume it with for await...of. This is the standard pattern for streaming data sources like paginated APIs, file line readers, and WebSocket streams.

⚠️
Generator objects are not reusable

Once a generator is exhausted (done: true), calling .next() will always return { value: undefined, done: true }. To restart, create a new generator by calling the generator function again.

Ad – 336Γ—280

πŸ‹οΈ Practical Exercise

Implement these generator utilities:

  • map*(gen, fn) β€” transforms each yielded value.
  • filter*(gen, predicate) β€” yields only values that pass the predicate.
  • zip*(gen1, gen2) β€” yields pairs [a, b] until either generator is done.
  • Chain them: filter even numbers from an infinite counter, map to squares, take first 5.

πŸ”₯ Challenge Exercise

Implement a generic pipeline(...generators) function that composes generator transformers left-to-right over an initial iterable. Then create a data processing pipeline that: reads a list of raw user objects β†’ filters active users β†’ maps to display names β†’ takes the first 10 β†’ collects to an array. All steps must be lazy generator functions.

Interview Questions

  • What is a generator function and how is it different from a regular function?
  • What does yield do? What does .next(value) do?
  • What is the iterator protocol ({ value, done })?
  • How do you make a custom class iterable with Symbol.iterator?
  • What is yield* and when would you use it?
  • What is the difference between a generator and an async generator?

πŸ“‹ Summary

  • Generator functions (function*) return a generator object; body runs only on .next().
  • yield expr pauses execution and sends expr as value to the caller.
  • .next(value) resumes the generator; value becomes the result of the yield expression.
  • Generators automatically implement the iterator protocol and are usable with for...of and spread.
  • Add [Symbol.iterator]*() to a class to make it iterable.
  • yield* delegates to another iterable/generator.
  • Async generators (async function*) combine await and yield for streaming async data.

Frequently Asked Questions

What is the difference between an iterator and an iterable? +

An iterable is any object with a [Symbol.iterator]() method that returns an iterator. An iterator is an object with a .next() method that returns { value, done }. Generator objects are both β€” they have [Symbol.iterator]() (returns this) and .next().

Can I use return inside a generator? +

Yes. return value inside a generator causes the generator to finish with { value: value, done: true }. Note that for...of ignores the done-true value β€” it only sees yielded values. The final return value is only visible if you call .next() manually after all yields are exhausted.

How do I throw an error into a generator? +

Call gen.throw(new Error('msg')). This resumes the generator as if the current yield threw the error. If the generator has a try/catch around the yield, it can handle the error and continue yielding. If not, the error propagates to the caller of .throw().