Lab 12: Iterators & Generators

Objective

Understand the JavaScript iteration protocol, create custom iterables, use generator functions to produce sequences lazily, and apply generators to real-world patterns like infinite sequences, async iteration, and pipelines.

Background

Iterators are the protocol behind for...of, spread [...arr], destructuring, and Array.from(). Generators are functions that can pause execution (yield) and resume later — enabling lazy evaluation, infinite sequences, and cooperative async control flow. Understanding these unlocks deeper JavaScript patterns used in libraries like Redux-Saga, RxJS, and async stream processing.

Time

40 minutes

Prerequisites

  • Lab 05 (Classes & OOP)

  • Lab 06 (Promises & Async/Await)

Tools

  • Node.js 20 LTS

  • Docker image: innozverse-js:latest


Lab Instructions

Step 1: The Iterator Protocol

Any object with a [Symbol.iterator]() method returning { next() { return { value, done } } } is iterable.

💡 Symbol.iterator is a well-known Symbol — a unique key that JavaScript uses to identify the iteration protocol. By defining it, your object plugs into the entire iteration ecosystem: for...of, spread, Array.from, Map, Set, destructuring.

📸 Verified Output:


Step 2: Generator Functions

Generator functions use function* and yield to pause and resume, returning a generator object.

💡 Generators are lazy by nature — they compute the next value only when .next() is called. This makes infinite sequences possible without infinite memory. The while(true) loop never runs away because yield pauses execution.

📸 Verified Output:


Step 3: yield* — Delegating to Other Generators

yield* delegates to another iterable, letting you compose generators.

💡 yield* recursion is the generator equivalent of recursive function calls. It's how you traverse trees, flatten structures, or chain sequences without building intermediate arrays.

📸 Verified Output:


Step 4: Two-way Communication — Sending Values into Generators

.next(value) sends a value back into the generator at the last yield point.

💡 The first .next() call always passes undefined because there's no previous yield to receive it. Think of it as "start the generator". Subsequent .next(value) calls resume from the yield and the expression yield result evaluates to value.

📸 Verified Output:


Step 5: Async Generators — Streaming Data

async function* yields Promises, enabling streaming over async sources.

💡 for await...of is the async equivalent of for...of — it awaits each yielded value. This is perfect for processing large datasets page-by-page, reading streams line-by-line, or polling APIs without loading everything into memory.

📸 Verified Output:


Step 6: Generator Pipeline — Lazy Data Processing

Chain generators to process data lazily without intermediate arrays.

💡 Generator pipelines are memory-efficient. When you chain filter → map → take over 1 million log lines, only one item flows through at a time. Arrays would allocate millions of entries. This is how Node.js streams and RxJS observables work under the hood.

📸 Verified Output:


Step 7: Return and Throw — Generator Control Flow

Generators can be terminated early with .return() or have errors injected with .throw().

💡 .throw(err) injects an error at the yield point, letting the generator handle it with try/catch. .return(value) forces completion and triggers finally blocks — critical for cleanup (closing files, releasing connections).

📸 Verified Output:


Step 8: Real-World — Paginated Data Collector

Build a practical async generator that collects all pages from a paginated API.

💡 This pattern is production-ready. Replace fetchProducts with an actual API call and you have a generic paginator that works with GitHub's API, Stripe's cursors, or any offset-based pagination — all without loading all pages into memory first.

📸 Verified Output:

(prices are randomized — values will differ)


Verification

Expected: All 25 products fetched across 5 pages, total value and premium count printed.

Common Mistakes

Mistake
Fix

Calling next() on exhausted generator

Check done before calling next() again

Forgetting first .next() initializes

First call runs to first yield, can't pass values

Using return inside generator loop

return ends the generator; use yield to continue

Forgetting * in function*

Without *, it's a regular function

Using for...of with async generator

Use for await...of for async generators

Summary

You now understand the iterator protocol, generator functions, yield* delegation, two-way communication via .next(value), async generators with for await...of, lazy pipelines, and early termination with .return()/.throw(). Generators are a superpower for elegant, memory-efficient data processing.

Further Reading

Last updated