Generators and Iterators

Course 3 · Ch 2
Generators and Iterators
Functions that can pause mid-execution and hand back control — the mechanism quietly powering for...of itself

Fundamentals Chapter 6 used for...of on arrays without explaining how it actually works under the hood. This chapter reveals the mechanism: the iterator protocol — and generator functions, a special syntax that makes writing a custom iterator dramatically simpler than doing it by hand.

The Iterator Protocol, By Hand

function makeRangeIterator(start, end) { let current = start; return { next() { if (current < end) { return { value: current++, done: false }; } return { value: undefined, done: true }; } }; } const it = makeRangeIterator(1, 4); console.log(it.next()); // { value: 1, done: false } console.log(it.next()); // { value: 2, done: false } console.log(it.next()); // { value: 3, done: false } console.log(it.next()); // { value: undefined, done: true }

An iterator is just an object with a next() method, returning { value, done } every call — Intermediate Chapter 2's closure pattern, used here to remember current between calls. for...of on an array calls exactly this kind of next() repeatedly behind the scenes, stopping once done becomes true.

Generator Functions — The Same Thing, Far Less Code

function* range(start, end) { for (let i = start; i < end; i++) { yield i; } } const it = range(1, 4); console.log(it.next()); // { value: 1, done: false } console.log(it.next()); // { value: 2, done: false }

function* (an asterisk after function) declares a generator function. Calling it doesn't run the body immediately — it returns an iterator automatically, with all the { value, done } bookkeeping handled for free. yield is the key new keyword: it pauses execution exactly where it appears, hands back a value, and resumes from that exact point the next time next() is called.

yield is genuinely a pause, not just a return
Unlike return, which ends a function permanently, yield freezes the function's local state — including the loop variable i above — and resumes from that exact line on the next next() call. This is the entire reason generators can produce values one at a time, on demand, rather than computing everything up front.

Using a Generator Directly with for...of

function* range(start, end) { for (let i = start; i < end; i++) { yield i; } } for (const n of range(1, 4)) { console.log(n); } // 1 2 3

Because a generator's return value already follows the iterator protocol, for...of works on it directly — no manual .next() calls needed at all. This is the practical payoff: write a generator once, then loop over it exactly like an array.

An Infinite Generator — Why "Lazy" Evaluation Matters

function* infiniteCounter() { let n = 1; while (true) { yield n++; } } const counter = infiniteCounter(); console.log(counter.next().value); // 1 console.log(counter.next().value); // 2 console.log(counter.next().value); // 3 // Could keep calling .next() forever — values are produced ON DEMAND, never all at once

infiniteCounter() contains a genuinely infinite while (true) loop, yet calling it doesn't hang the program — nothing inside the loop body actually runs until .next() is explicitly called. This "lazy" behaviour (computing only what's actually requested) is impossible with a regular array, which would need to exist completely in memory before any of it could be used.

Never use for...of on an infinite generator directly
for (const n of infiniteCounter()) would loop forever, since for...of keeps calling .next() until done becomes true — which never happens here. Infinite generators must be driven manually with .next(), typically combined with a separate stopping condition.

ConceptDescription
IteratorAny object with a next() method returning { value, done }
function* name() { }A generator function; calling it returns an iterator automatically
yield valuePauses the generator, hands back a value, remembers where it left off
for (const x of generatorCall())Works directly — generators already satisfy the iterator protocol
Lazy evaluationValues are computed only when actually requested via next()

Coding Challenges

Challenge 1

Write a generator function* evens(max) that yields every even number from 0 up to max (inclusive). Use for...of to print every value it produces for evens(10).

📄 View solution
Challenge 2

Write a generator function* fibonacci() that yields an infinite sequence of Fibonacci numbers (1, 1, 2, 3, 5, 8, ...), starting from two seed values. Manually call .next().value six times on an instance and log each result, WITHOUT using for...of (since it's infinite).

📄 View solution
Challenge 3

Write a generator function* idGenerator(prefix) that yields strings like "prefix-1", "prefix-2", "prefix-3", incrementing forever. Create two SEPARATE generators with different prefixes ("user" and "order") and call .next().value three times on each, interleaved, to confirm they don't interfere with each other (similar in spirit to Intermediate Chapter 2's closure challenge).

📄 View solution

Chapter 2 Quick Reference

  • Iterator — any object with next(), returning { value, done }
  • function* name() { } — generator function syntax; the asterisk is required
  • yield value — pauses, returns a value, remembers exactly where execution stopped
  • Calling a generator returns an iterator immediately; the body doesn't run until .next() is called
  • for...of works directly on a generator's return value — no manual .next() needed for finite generators
  • Infinite generators are safe — nothing runs until .next() actually requests a value
  • Never for...of an infinite generator — drive it manually with .next() instead
  • Next chapter: the event loop in depth — microtasks vs macrotasks