Day 25: Generators & Iterators
Day 25: Generators & Iterators
1. Learning Objectives
By the end of this lesson, you will be able to:
- Understand what iterators and iterables are in JavaScript
- Create custom iterables by implementing the
Symbol.iteratormethod - Define generator functions using
function*syntax - Use
yieldto produce values lazily from generators - Consume generators with
for...of, spread operator, and destructuring - Delegate generators using
yield* - Pass values back into generators using
next(value) - Recognize practical use cases for generators (infinite sequences, async flows, data processing)
2. Theory
What are Iterators and Iterables?
An iterable is an object that defines a sequence of values that can be iterated over. JavaScript has many built-in iterables: arrays, strings, Maps, Sets, etc.
An iterator is an object that implements the iterator protocol — it has a next() method that returns an object with two properties:
value— the next value in the sequencedone—trueif the sequence is finished,falseotherwise
// Manual iterator example
const array = [10, 20, 30];
const iterator = array[Symbol.iterator]();
console.log(iterator.next()); // { value: 10, done: false }
console.log(iterator.next()); // { value: 20, done: false }
console.log(iterator.next()); // { value: 30, done: false }
console.log(iterator.next()); // { value: undefined, done: true }
The Iterable Protocol
An object is iterable if it has a method at Symbol.iterator that returns an iterator.
const iterable = {
[Symbol.iterator]() {
let count = 0;
return {
next() {
count++;
if (count <= 3) {
return { value: count * 10, done: false };
}
return { value: undefined, done: true };
}
};
}
};
for (const value of iterable) {
console.log(value); // 10, 20, 30
}
Generator Functions
Generator functions provide a simpler way to create iterators. They are defined with function* and use the yield keyword.
function* simpleGenerator() {
yield 1;
yield 2;
yield 3;
}
const gen = simpleGenerator();
console.log(gen.next()); // { value: 1, done: false }
console.log(gen.next()); // { value: 2, done: false }
console.log(gen.next()); // { value: 3, done: false }
console.log(gen.next()); // { value: undefined, done: true }
Key Characteristics
- Lazy evaluation — values are produced only when requested via
.next() - Stateful — the generator remembers its position between calls
- Pausable — execution pauses at each
yieldand resumes on the next.next() - Two-way communication — you can pass values back into generators
// Generators are iterable
function* countUp() {
yield 1;
yield 2;
yield 3;
}
// for...of works automatically
for (const num of countUp()) {
console.log(num); // 1, 2, 3
}
// Spread works too
const numbers = [...countUp()];
console.log(numbers); // [1, 2, 3]
// Destructuring
const [a, b, c] = countUp();
console.log(a, b, c); // 1 2 3
yield* Delegation
The yield* expression delegates to another iterable or generator:
function* numbers() {
yield 1;
yield 2;
yield 3;
}
function* letters() {
yield 'A';
yield 'B';
}
function* combined() {
yield* numbers();
yield* letters();
yield* [10, 20, 30];
yield* "Hi";
}
console.log([...combined()]);
// [1, 2, 3, 'A', 'B', 10, 20, 30, 'H', 'i']
Passing Values into Generators
You can send values back into a generator using generator.next(value). The value passed becomes the result of the yield expression inside the generator.
function* interactive() {
const name = yield "What is your name?";
const age = yield `Hello ${name}, how old are you?`;
yield `So you are ${age} years old. Nice to meet you!`;
}
const gen = interactive();
console.log(gen.next()); // { value: "What is your name?", done: false }
console.log(gen.next("Alice")); // { value: "Hello Alice, how old are you?", done: false }
console.log(gen.next(28)); // { value: "So you are 28 years old...", done: false }
console.log(gen.next()); // { value: undefined, done: true }
Infinite Generators
Generators can produce infinite sequences because they are lazy:
function* fibonacci() {
let a = 0, b = 1;
while (true) {
yield a;
[a, b] = [b, a + b];
}
}
const fib = fibonacci();
console.log(fib.next().value); // 0
console.log(fib.next().value); // 1
console.log(fib.next().value); // 1
console.log(fib.next().value); // 2
console.log(fib.next().value); // 3
console.log(fib.next().value); // 5
// ... can continue forever
// Take only first 10
const first10 = [];
for (const n of fibonacci()) {
if (first10.length >= 10) break;
first10.push(n);
}
console.log(first10); // [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
return() and throw() Methods
Generators also have return() and throw() methods:
function* countdown(start) {
while (start > 0) {
yield start;
start--;
}
}
const gen = countdown(5);
console.log(gen.next()); // { value: 5, done: false }
console.log(gen.next()); // { value: 4, done: false }
console.log(gen.return(99)); // { value: 99, done: true } — terminates early
console.log(gen.next()); // { value: undefined, done: true }
3. Code Examples
Example 1: Creating Custom Iterables
// ---- Custom Range Iterable ----
class Range {
constructor(start, end, step = 1) {
this.start = start;
this.end = end;
this.step = step;
}
[Symbol.iterator]() {
let current = this.start;
const end = this.end;
const step = this.step;
return {
next() {
if ((step > 0 && current <= end) || (step < 0 && current >= end)) {
const value = current;
current += step;
return { value, done: false };
}
return { value: undefined, done: true };
}
};
}
}
// Usage
console.log("Range 1 to 5:");
for (const n of new Range(1, 5)) {
console.log(n); // 1, 2, 3, 4, 5
}
console.log("\nRange 10 to 0 (step -2):");
for (const n of new Range(10, 0, -2)) {
console.log(n); // 10, 8, 6, 4, 2, 0
}
// Spread works
console.log("\nSpread range:", [...new Range(3, 8)]); // [3, 4, 5, 6, 7, 8]
// ---- Custom Pagination Iterable ----
class PaginatedAPI {
constructor(baseUrl, pageSize = 10) {
this.baseUrl = baseUrl;
this.pageSize = pageSize;
this.cache = new Map();
}
async fetchPage(page) {
if (this.cache.has(page)) return this.cache.get(page);
const response = await fetch(`${this.baseUrl}?_page=${page}&_limit=${this.pageSize}`);
const data = await response.json();
this.cache.set(page, data);
return data;
}
[Symbol.asyncIterator]() {
let page = 1;
let hasMore = true;
return {
next: async () => {
if (!hasMore) return { done: true };
const data = await this.fetchPage(page);
page++;
if (data.length === 0) {
hasMore = false;
return { done: true };
}
return { value: data, done: false };
}
};
}
}
Example 2: Generator Basics
// ---- Basic Generator Examples ----
function* idGenerator() {
let id = 1;
while (true) {
yield id++;
}
}
const ids = idGenerator();
console.log(ids.next().value); // 1
console.log(ids.next().value); // 2
console.log(ids.next().value); // 3
// Generator that yields from an array
function* arrayIterator(arr) {
for (let i = 0; i < arr.length; i++) {
yield arr[i];
}
}
const names = ["Alice", "Bob", "Charlie"];
for (const name of arrayIterator(names)) {
console.log(name); // Alice, Bob, Charlie
}
// Generator with multiple yields
function* threeActs() {
yield "Act 1: Setup";
yield "Act 2: Confrontation";
yield "Act 3: Resolution";
}
const play = threeActs();
console.log([...play]); // ["Act 1: Setup", "Act 2: Confrontation", "Act 3: Resolution"]
Example 3: Generator Delegation with yield*
// ---- yield* Delegation Examples ----
function* vegetables() {
yield "Carrot";
yield "Broccoli";
yield "Spinach";
}
function* fruits() {
yield "Apple";
yield "Banana";
yield "Cherry";
}
function* groceryList() {
yield "=== Vegetables ===";
yield* vegetables();
yield "";
yield "=== Fruits ===";
yield* fruits();
yield "";
yield "=== Dairy ===";
yield* ["Milk", "Cheese", "Yogurt"];
}
console.log("Grocery List:");
for (const item of groceryList()) {
console.log(item);
}
// === Vegetables ===
// Carrot
// Broccoli
// Spinach
//
// === Fruits ===
// Apple
// Banana
// Cherry
//
// === Dairy ===
// Milk
// Cheese
// Yogurt
// ---- Deep tree traversal with yield* ----
const tree = {
name: "Root",
children: [
{
name: "Child 1",
children: [
{ name: "Grandchild 1.1", children: [] },
{ name: "Grandchild 1.2", children: [] }
]
},
{
name: "Child 2",
children: [
{ name: "Grandchild 2.1", children: [] }
]
}
]
};
function* traverseTree(node) {
yield node.name;
for (const child of node.children) {
yield* traverseTree(child);
}
}
console.log("\nTree traversal:");
for (const name of traverseTree(tree)) {
console.log(` ${name}`);
}
// Root, Child 1, Grandchild 1.1, Grandchild 1.2, Child 2, Grandchild 2.1
Example 4: Two-Way Communication with Generators
// ---- Interactive Generator ----
function* quizGame() {
const answer1 = yield "What is 2 + 2?";
if (answer1 === "4") {
yield "Correct! Next question...";
} else {
yield "Wrong! The answer was 4. Next question...";
}
const answer2 = yield "What is the capital of France?";
if (answer2.toLowerCase() === "paris") {
yield "Correct! You're doing great!";
} else {
yield "The capital is Paris. Keep trying!";
}
yield "Quiz complete! Thanks for playing.";
}
const game = quizGame();
console.log(game.next().value); // "What is 2 + 2?"
console.log(game.next("4").value); // "Correct! Next question..."
console.log(game.next("Paris").value); // "What is the capital of France?"
console.log(game.next("paris").value); // "Correct! You're doing great!"
console.log(game.next().value); // "Quiz complete! Thanks for playing."
// ---- Data Processor with Two-Way Communication ----
function* dataProcessor() {
let total = 0;
let count = 0;
while (true) {
const value = yield { total, count, average: count > 0 ? total / count : 0 };
if (value === null) break; // Signal to stop
total += value;
count++;
}
return { finalTotal: total, finalCount: count };
}
const processor = dataProcessor();
console.log(processor.next()); // { value: { total: 0, count: 0, average: 0 }, done: false }
console.log(processor.next(10)); // { value: { total: 10, count: 1, average: 10 }, done: false }
console.log(processor.next(20)); // { value: { total: 30, count: 2, average: 15 }, done: false }
console.log(processor.next(30)); // { value: { total: 60, count: 3, average: 20 }, done: false }
console.log(processor.next(null)); // { value: { finalTotal: 60, finalCount: 3 }, done: true }
Example 5: Practical Generator Use Cases
// ---- 1. Infinite Sequence: Unique ID Generator ----
function* uniqueIdGenerator(prefix = "id") {
let counter = 0;
while (true) {
yield `${prefix}_${counter++}`;
}
}
const ids = uniqueIdGenerator("user");
console.log(ids.next().value); // "user_0"
console.log(ids.next().value); // "user_1"
console.log(ids.next().value); // "user_2"
// ---- 2. Pagination Helper ----
function* paginate(array, pageSize) {
let index = 0;
while (index < array.length) {
yield array.slice(index, index + pageSize);
index += pageSize;
}
}
const items = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const pages = paginate(items, 3);
for (const page of pages) {
console.log("Page:", page);
}
// Page: [1, 2, 3]
// Page: [4, 5, 6]
// Page: [7, 8, 9]
// Page: [10]
// ---- 3. Lazy Sequence: Even Numbers ----
function* evenNumbers() {
let n = 0;
while (true) {
yield n;
n += 2;
}
}
// Take first 5 even numbers
const firstFive = [];
for (const even of evenNumbers()) {
if (firstFive.length >= 5) break;
firstFive.push(even);
}
console.log("First 5 evens:", firstFive); // [0, 2, 4, 6, 8]
// ---- 4. Async Generator: Simulated Data Stream ----
async function* dataStream() {
let i = 0;
while (i < 5) {
await new Promise(resolve => setTimeout(resolve, 500));
yield `Data chunk ${i++}`;
}
}
async function consumeStream() {
for await (const chunk of dataStream()) {
console.log("Received:", chunk);
}
console.log("Stream complete!");
}
// consumeStream(); // Uncomment to run (takes ~2.5 seconds)
// ---- 5. Generator as State Machine ----
function* trafficLight() {
while (true) {
yield "🟢 Green";
yield "🟡 Yellow";
yield "🔴 Red";
}
}
const light = trafficLight();
console.log(light.next().value); // 🟢 Green
console.log(light.next().value); // 🟡 Yellow
console.log(light.next().value); // 🔴 Red
console.log(light.next().value); // 🟢 Green (cycles)
4. Exercises
Beginner
- Create a generator function
countToThreethat yields 1, 2, 3. Iterate over it withfor...of. - Create a generator
range(start, end)that yields numbers fromstarttoend(inclusive). - Use
yield*to delegate to an array inside a generator.
Intermediate
- Write a generator
fibonacci(n)that yields the firstnFibonacci numbers. - Create a generator
take(n, iterable)that yields the firstnvalues from any iterable. - Write a generator
cycle(iterable)that infinitely cycles through the values of an iterable. (Be careful with infinite loops!)
Advanced
- Implement a lazy map generator:
function* lazyMap(iterable, fn)that yields transformed values one at a time without creating an intermediate array. - Build a simple reactive stream using generators: create a
Subjectthat maintains a list of generator observers and pushes values to all of them.
5. Mini Project: Lazy Data Processing Pipeline
Build a data processing pipeline using generators for lazy evaluation — values flow through the pipeline one at a time without creating intermediate arrays.
// === Lazy Data Processing Pipeline ===
// ---- Generator Utilities ----
function* range(start, end, step = 1) {
for (let i = start; i <= end; i += step) {
yield i;
}
}
function* map(iterable, fn) {
for (const value of iterable) {
yield fn(value);
}
}
function* filter(iterable, predicate) {
for (const value of iterable) {
if (predicate(value)) {
yield value;
}
}
}
function* take(iterable, count) {
let taken = 0;
for (const value of iterable) {
if (taken >= count) return;
yield value;
taken++;
}
}
function* skip(iterable, count) {
let skipped = 0;
for (const value of iterable) {
if (skipped >= count) {
yield value;
}
skipped++;
}
}
function* enumerate(iterable) {
let index = 0;
for (const value of iterable) {
yield [index, value];
index++;
}
}
function reduce(iterable, fn, initial) {
let accumulator = initial;
let first = true;
for (const value of iterable) {
if (first && initial === undefined) {
accumulator = value;
first = false;
} else {
accumulator = fn(accumulator, value);
}
}
return accumulator;
}
function toArray(iterable) {
return [...iterable];
}
// ---- Pipeline Demo ----
console.log("=== Lazy Data Processing Pipeline ===\n");
// Build a pipeline: range(1, 100) → filter even → map square → take 5
const pipeline = take(
map(
filter(range(1, 100), n => n % 2 === 0),
n => n * n
),
5
);
console.log("Pipeline results (first 5 even squares from 1-100):");
for (const value of pipeline) {
console.log(` ${value}`);
}
// 4, 16, 36, 64, 100
// ---- More Complex Pipeline ----
console.log("\n--- Complex Pipeline: Process user data ---\n");
// Simulated large dataset (would be 1M+ in real use)
function* generateUsers(count) {
const firstNames = ["Alice", "Bob", "Charlie", "Diana", "Eve", "Frank"];
const lastNames = ["Smith", "Johnson", "Williams", "Brown", "Jones"];
for (let i = 0; i < count; i++) {
yield {
id: i + 1,
firstName: firstNames[i % firstNames.length],
lastName: lastNames[i % lastNames.length],
age: 18 + (i % 50),
email: `user${i + 1}@example.com`
};
}
}
// Pipeline: generate 100 users → filter adults 18+ → map to full name + age group → skip 5 → take 3
const adultUsers = take(
skip(
map(
filter(generateUsers(100), user => user.age >= 18),
user => ({
fullName: `${user.firstName} ${user.lastName}`,
age: user.age,
ageGroup: user.age < 30 ? "Young Adult" :
user.age < 50 ? "Adult" : "Senior",
email: user.email
})
),
5
),
3
);
console.log("Processed users (skip 5, take 3):");
for (const user of adultUsers) {
console.log(` ${user.fullName} | ${user.age} (${user.ageGroup}) | ${user.email}`);
}
// ---- Reduce Example ----
console.log("\n--- Statistics using reduce on pipeline ---");
const stats = reduce(
take(
map(
filter(range(1, 1000), n => n % 2 !== 0), // odd numbers
n => n * n // squared
),
50 // first 50 odd squares
),
(acc, val) => ({
sum: acc.sum + val,
count: acc.count + 1,
min: Math.min(acc.min, val),
max: Math.max(acc.max, val)
}),
{ sum: 0, count: 0, min: Infinity, max: -Infinity }
);
console.log("Statistics of first 50 odd squares:");
console.log(` Count: ${stats.count}`);
console.log(` Sum: ${stats.sum}`);
console.log(` Min: ${stats.min}`);
console.log(` Max: ${stats.max}`);
console.log(` Average: ${(stats.sum / stats.count).toFixed(2)}`);
// ---- Lazy vs Eager Performance Comparison ----
console.log("\n--- Performance: Lazy vs Eager ---\n");
// Eager approach (creates intermediate arrays)
function eagerProcess() {
const range = [];
for (let i = 1; i <= 10000; i++) range.push(i);
const evens = range.filter(n => n % 2 === 0);
const squared = evens.map(n => n * n);
const result = squared.slice(0, 5);
return result;
}
// Lazy approach (no intermediate arrays)
function lazyProcess() {
return take(
map(
filter(range(1, 10000), n => n % 2 === 0),
n => n * n
),
5
);
}
console.time("Eager (creates arrays)");
const eagerResult = eagerProcess();
console.timeEnd("Eager (creates arrays)");
console.time("Lazy (generators)");
const lazyResult = [...lazyProcess()];
console.timeEnd("Lazy (generators)");
console.log("Both produce:", eagerResult, lazyResult);
Try extending it: Add zip, flatMap, distinct, chunk, sort (with buffer), or groupBy as generator utilities. Benchmark larger datasets to see the memory difference.
6. Common Mistakes
| Mistake | Why it's wrong | Correct approach |
|---|---|---|
Forgetting * in function* | Creates a regular function instead of a generator | Always use function* name() {} |
Using return instead of yield | return ends the generator immediately with done: true | Use yield to produce values; use return only for final value |
| Trying to reuse a generator | A generator can only be iterated once; subsequent calls return empty | Create a new generator instance each time |
| Expecting generators to be async by default | Generators are synchronous unless explicitly defined as async | Use async function*() and for await...of for async |
| Modifying the underlying data while iterating | Can cause unexpected behavior or skipped values | Iterate over a copy if data might change |
Using yield inside a callback or arrow function | yield is only valid inside generator functions | Ensure the function containing yield is declared with function* |
| Not handling infinite generators carefully | for...of on an infinite generator loops forever | Use take(), break, or return() to terminate |
Forgetting that [...gen] consumes the entire generator | For infinite generators, this causes an infinite loop | Use take() or a loop with break for partial consumption |
7. Best Practices
- Use generators for lazy sequences — when you don't want to compute or store all values at once.
- Use generators for infinite sequences — mathematical sequences, unique IDs, cyclic patterns.
- Use generators for state machines — traffic lights, game states, workflow steps.
- Use
yield*for delegation — compose multiple generators or iterate over nested structures. - Use generators for data processing pipelines — chain
map,filter,takewithout intermediate arrays. - Always handle infinite generators with a
take()utility or explicitbreakcondition. - Create a new generator instance each time you need to iterate from the beginning.
- Use
for...offor clean consumption — it calls.next()and checksdoneautomatically. - Use
async generatorsfor streaming data — API pagination, file reading, WebSocket messages. - Name your generators clearly — use noun-based names like
idGenerator,fibonacciSequence.
8. Challenge Assignment (Optional)
"Async Data Stream Processor" Challenge
Build an async generator-based system that processes a stream of data from multiple sources.
Requirements:
Create an
async function* dataSource(url, interval)that:- Simulates fetching data from a URL at a given interval
- Yields each chunk of data as it arrives
- Handles errors gracefully (logs and continues)
Create async generator transforms:
filterAsync(source, predicate)— async filtermapAsync(source, transform)— async mapbatch(source, size)— collects items into batches ofsizedebounce(source, ms)— only yields aftermsof inactivity
Create a
StreamProcessorclass that:- Connects multiple sources and transforms
- Provides
pipe(transform)method for chaining - Provides
subscribe(callback)to consume the final output - Provides
start()andstop()methods - Tracks statistics (items processed, errors, throughput)
Bonus:
- Add a
retrymechanism for failed source fetches - Add a
rateLimittransform - Implement backpressure handling
- Visualize the stream in a browser UI with real-time updates
Example usage:
const processor = new StreamProcessor()
.fromSource(dataSource("/api/events", 1000))
.pipe(filterAsync(event => event.type === "user_action"))
.pipe(mapAsync(event => ({ ...event, timestamp: Date.now() })))
.pipe(batch(10))
.pipe(debounce(500))
.subscribe(batch => console.log("Batch received:", batch));
processor.start();
// After 30 seconds:
processor.stop();
console.log(processor.getStats());
// { received: 30, processed: 22, errors: 3, throughput: 0.73 items/sec }
9. Knowledge Check (Quiz)
What does a generator function return when called?
- a) An array of yielded values
- b) An iterator object with a
next()method - c) The first yielded value
- d)
undefined
Which keyword is used to produce values from a generator?
- a)
return - b)
emit - c)
yield - d)
produce
- a)
What is the output of this code?
function* gen() { yield 1; yield 2; } const g = gen(); console.log(g.next()); console.log(g.next()); console.log(g.next());- a)
1,2,undefined - b)
{ value: 1, done: false },{ value: 2, done: false },{ value: undefined, done: true } - c)
{ value: 1, done: true },{ value: 2, done: true },{ value: undefined, done: true } - d)
[1, 2],[],[]
- a)
What does
yield*do?- a) Yields the current value multiplied by itself
- b) Delegates to another iterable or generator
- c) Yields all remaining values at once
- d) Stops the generator
How do you pass a value back into a generator?
- a)
generator.send(value) - b)
generator.next(value) - c)
generator.emit(value) - d)
generator.yield(value)
- a)
What is the key advantage of generators over arrays for large sequences?
- a) Generators are faster
- b) Generators are lazy — they don't store all values in memory
- c) Generators support async operations
- d) Both b and c
What happens if you iterate over an infinite generator with
for...of?- a) It runs forever (infinite loop)
- b) It stops after 10000 iterations
- c) It throws an error
- d) It only processes the first value
How do you consume an async generator?
- a)
for (const value of gen) {} - b)
for await (const value of gen) {} - c)
while (gen.next()) {} - d)
gen.forEach(value => {})
- a)
Answers: 1-b, 2-c, 3-b, 4-b, 5-b, 6-d, 7-a, 8-b
10. Additional Resources
- MDN: Iterators and generators → developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Iterators_and_Generators
- MDN: Generator → developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Generator
- MDN: yield → developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/yield
- MDN: yield* → developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/yield*
- MDN: Symbol.iterator → developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/iterator
- JavaScript.info: Generators → javascript.info/generators
- JavaScript.info: Async iterators → javascript.info/async-iterators-generators
- Video: "JavaScript Generators" by Web Dev Simplified
- Video: "JavaScript Generators — They're Not Weird" by Fun Fun Function
- Practice: freeCodeCamp: ES6 Generators
- Book: Chapter 4 of Exploring ES6 by Dr. Axel Rauschmayer
11. Summary
Today you learned:
- Iterables and iterators — objects that define sequences via
Symbol.iteratorand thenext()method - Generator functions —
function*withyieldfor creating iterators more easily - Lazy evaluation — values are produced only when requested, saving memory
yield*delegation — composing generators and delegating to other iterables- Two-way communication — passing values into generators via
next(value) - Infinite generators — sequences that never end (Fibonacci, IDs, counters)
- Generator methods —
return()andthrow()for early termination and error injection - Async generators —
async function*withfor await...offor streaming data - Practical applications — unique IDs, pagination, lazy data pipelines, state machines, data streams
You now have a powerful tool for lazy, memory-efficient data processing. Generators bridge the gap between simple arrays and complex asynchronous streams. Tomorrow we'll dive into the Event Loop and concurrency — understanding how JavaScript handles multiple tasks behind the scenes.
"Generators are the lazy bones of JavaScript — they only work when you ask them to. And that's exactly what makes them powerful."

Comments
Post a Comment