Imagine you're a master storyteller 📖 sitting around a campfire with an eager audience. Unlike reading a book where pages are turned in sequence, your storytelling has special powers:
Iterators and generators work exactly like this master storyteller. They give you complete control over how data is accessed and produced, allowing you to create:
This isn't just syntactic sugar - iterators and generators represent a fundamental shift toward more sophisticated control flow patterns that enable elegant solutions to complex problems.
Iterators and generators implement "lazy evaluation" - a fundamental concept in computer science where computations are deferred until their results are actually needed.
This contrasts with eager evaluation (the default in most languages) where expressions are evaluated immediately when encountered.
Why Lazy Evaluation Matters:
Real-World Analogy: Think of lazy evaluation like a smart restaurant. Instead of preparing every dish on the menu at opening time (eager), they prepare dishes only when customers order them (lazy). This saves ingredients, reduces waste, and ensures freshness.
Iterators implement the "Iterator Pattern" from design patterns, providing a uniform interface for traversing collections without exposing their internal structure.
Core Concepts:
Generators implement "cooperative multitasking" - a concurrency model where functions voluntarily yield control rather than being preemptively interrupted.
Theoretical Background:
This enables:
Traditional iteration is "pull-based" - the consumer pulls values from the producer:
Consumer: "Give me the next value"
Producer: "Here it is" or "I'm done"
Generators enable "push-like" patterns where the producer can control the flow:
Producer: "Here's a value, process it"
Consumer: "OK, give me the next one when ready"
This inverts control and enables more sophisticated data processing patterns like reactive programming and stream processing.
Before iterators, JavaScript had limited ways to make objects iterable:
// Traditional array iteration
const numbers = [1, 2, 3, 4, 5];
for (let i = 0; i < numbers.length; i++) {
console.log(numbers[i]);
}
// Object iteration required Object.keys()
const user = { name: "Alice", age: 30, city: "New York" };
for (let key of Object.keys(user)) {
console.log(key, user[key]);
}
// Custom objects couldn't be iterated naturally
const customCollection = {
data: [1, 2, 3, 4, 5],
// No built-in way to make this object work with for...of
};
// This doesn't work:
// for (let value of customCollection) { ... } // TypeError!
Problems with traditional approaches:
The iterator protocol provides a standard way to define how objects are iterated:
// Iterator protocol: objects must have a Symbol.iterator method
// that returns an iterator (object with next() method)
const customIterable = {
data: [1, 2, 3, 4, 5],
// Symbol.iterator method makes the object iterable
[Symbol.iterator]() {
let index = 0;
const data = this.data;
// Return an iterator object
return {
next() {
if (index < data.length) {
return { value: data[index++], done: false };
} else {
return { done: true };
}
}
};
}
};
// Now it works with for...of!
for (let value of customIterable) {
console.log(value); // 1, 2, 3, 4, 5
}
// Also works with spread operator
const array = [...customIterable]; // [1, 2, 3, 4, 5]
// And with destructuring
const [first, second, ...rest] = customIterable;
console.log(first, second, rest); // 1, 2, [3, 4, 5]
Key Components:
Symbol.iterator
methodnext()
methodvalue
and done
properties// Understanding the protocol step by step
function createNumberIterator(start, end) {
let current = start;
// Return an iterator object
return {
next() {
if (current <= end) {
// Return { value: someValue, done: false }
return { value: current++, done: false };
} else {
// Return { done: true } when iteration is complete
return { done: true };
}
}
};
}
// Manual iteration using the iterator
const iterator = createNumberIterator(1, 3);
console.log(iterator.next()); // { value: 1, done: false }
console.log(iterator.next()); // { value: 2, done: false }
console.log(iterator.next()); // { value: 3, done: false }
console.log(iterator.next()); // { done: true }
// Creating an iterable object that uses our iterator
const numberRange = {
start: 1,
end: 5,
[Symbol.iterator]() {
return createNumberIterator(this.start, this.end);
}
};
console.log([...numberRange]); // [1, 2, 3, 4, 5]
Example 1: Fibonacci Sequence Iterator
function createFibonacciIterator(maxCount = Infinity) {
let prev = 0;
let current = 1;
let count = 0;
return {
next() {
if (count >= maxCount) {
return { done: true };
}
if (count === 0) {
count++;
return { value: prev, done: false };
}
if (count === 1) {
count++;
return { value: current, done: false };
}
const next = prev + current;
prev = current;
current = next;
count++;
return { value: prev, done: false };
}
};
}
// Create an iterable Fibonacci sequence
const fibonacci = {
maxCount: 10,
[Symbol.iterator]() {
return createFibonacciIterator(this.maxCount);
}
};
console.log([...fibonacci]); // [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
// Or create infinite sequence (careful with this!)
const infiniteFibonacci = {
[Symbol.iterator]() {
return createFibonacciIterator(); // No limit
}
};
// Take first 15 Fibonacci numbers
const first15 = [];
for (let num of infiniteFibonacci) {
if (first15.length >= 15) break;
first15.push(num);
}
console.log(first15); // [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377]
Example 2: Tree Node Iterator
class TreeNode {
constructor(value, children = []) {
this.value = value;
this.children = children;
}
// Depth-first traversal iterator
*depthFirst() {
yield this.value;
for (let child of this.children) {
yield* child.depthFirst();
}
}
// Breadth-first traversal iterator
*breadthFirst() {
const queue = [this];
while (queue.length > 0) {
const node = queue.shift();
yield node.value;
queue.push(...node.children);
}
}
// Make the tree iterable (default to depth-first)
[Symbol.iterator]() {
return this.depthFirst();
}
}
// Create a tree structure
const tree = new TreeNode("root", [
new TreeNode("child1", [
new TreeNode("grandchild1"),
new TreeNode("grandchild2")
]),
new TreeNode("child2", [
new TreeNode("grandchild3")
])
]);
// Iterate using default (depth-first)
console.log("Depth-first:", [...tree]);
// ["root", "child1", "grandchild1", "grandchild2", "child2", "grandchild3"]
// Iterate using breadth-first
console.log("Breadth-first:", [...tree.breadthFirst()]);
// ["root", "child1", "child2", "grandchild1", "grandchild2", "grandchild3"]
What are generators? Special functions that can pause and resume their execution, automatically implementing the iterator protocol.
Mental Model: Think of generators like a remote control for function execution - you can play, pause, rewind, and even send information back to the function while it's paused.
// Generator function syntax: function* (note the asterisk)
function* simpleGenerator() {
console.log("Generator started");
yield 1;
console.log("After first yield");
yield 2;
console.log("After second yield");
yield 3;
console.log("Generator ending");
return "Done!";
}
// Calling a generator function returns a generator object
const gen = simpleGenerator();
console.log(gen); // [object Generator]
// Use next() to advance through the generator
console.log(gen.next()); // "Generator started" -> { value: 1, done: false }
console.log(gen.next()); // "After first yield" -> { value: 2, done: false }
console.log(gen.next()); // "After second yield" -> { value: 3, done: false }
console.log(gen.next()); // "Generator ending" -> { value: "Done!", done: true }
console.log(gen.next()); // { value: undefined, done: true }
// Generators are automatically iterable
function* numberGenerator() {
yield 10;
yield 20;
yield 30;
}
// Works with for...of
for (let num of numberGenerator()) {
console.log(num); // 10, 20, 30
}
// Works with spread operator
const numbers = [...numberGenerator()]; // [10, 20, 30]
// Works with destructuring
const [first, second, third] = numberGenerator();
console.log(first, second, third); // 10, 20, 30
Feature 1: Yielding Values and Expressions
function* mathGenerator() {
yield 1 + 1; // yield expressions
yield Math.PI; // yield computed values
yield new Date().getFullYear(); // yield function results
const array = [1, 2, 3];
yield* array; // yield* delegates to another iterable
yield* "hello"; // strings are iterable
}
console.log([...mathGenerator()]);
// [2, 3.14159..., 2025, 1, 2, 3, "h", "e", "l", "l", "o"]
Feature 2: Two-Way Communication
function* interactiveGenerator() {
const input1 = yield "What's your name?";
console.log(`Hello, ${input1}!`);
const input2 = yield "What's your age?";
console.log(`You are ${input2} years old.`);
const input3 = yield "What's your favorite color?";
return `Nice to meet you, ${input1}! ${input3} is a great color.`;
}
const gen = interactiveGenerator();
console.log(gen.next()); // { value: "What's your name?", done: false }
console.log(gen.next("Alice")); // "Hello, Alice!" -> { value: "What's your age?", done: false }
console.log(gen.next(30)); // "You are 30 years old." -> { value: "What's your favorite color?", done: false }
console.log(gen.next("Blue")); // { value: "Nice to meet you, Alice! Blue is a great color.", done: true }
Feature 3: Error Handling in Generators
function* errorHandlingGenerator() {
try {
yield "Step 1";
yield "Step 2";
yield "Step 3";
} catch (error) {
console.log("Caught error:", error.message);
yield "Error handled";
}
yield "Continuing after error";
}
const gen = errorHandlingGenerator();
console.log(gen.next()); // { value: "Step 1", done: false }
console.log(gen.next()); // { value: "Step 2", done: false }
console.log(gen.throw(new Error("Something went wrong")));
// "Caught error: Something went wrong" -> { value: "Error handled", done: false }
console.log(gen.next()); // { value: "Continuing after error", done: false }
Example 1: Infinite Sequence Generators
// Infinite counter
function* counter(start = 0, step = 1) {
let current = start;
while (true) {
yield current;
current += step;
}
}
// Infinite random number generator
function* randomNumbers(min = 0, max = 1) {
while (true) {
yield Math.random() * (max - min) + min;
}
}
// Infinite ID generator
function* idGenerator(prefix = "id") {
let counter = 1;
while (true) {
yield `${prefix}-${counter++}`;
}
}
// Usage with controlled iteration
const count = counter(0, 2);
for (let i = 0; i < 5; i++) {
console.log(count.next().value); // 0, 2, 4, 6, 8
}
const ids = idGenerator("user");
console.log(ids.next().value); // "user-1"
console.log(ids.next().value); // "user-2"
console.log(ids.next().value); // "user-3"
// Take first N values from infinite sequence
function take(generator, n) {
const result = [];
for (let i = 0; i < n; i++) {
const { value, done } = generator.next();
if (done) break;
result.push(value);
}
return result;
}
const randomGen = randomNumbers(1, 10);
console.log(take(randomGen, 5)); // [random numbers between 1-10]
Example 2: Data Processing Pipeline
// Generator-based data processing pipeline
function* filterGenerator(iterable, predicate) {
for (let item of iterable) {
if (predicate(item)) {
yield item;
}
}
}
function* mapGenerator(iterable, transform) {
for (let item of iterable) {
yield transform(item);
}
}
function* takeWhileGenerator(iterable, predicate) {
for (let item of iterable) {
if (predicate(item)) {
yield item;
} else {
break;
}
}
}
function* chunkGenerator(iterable, size) {
let chunk = [];
for (let item of iterable) {
chunk.push(item);
if (chunk.length === size) {
yield chunk;
chunk = [];
}
}
if (chunk.length > 0) {
yield chunk;
}
}
// Create a data processing pipeline
function* processNumbers(numbers) {
const filtered = filterGenerator(numbers, x => x > 0);
const squared = mapGenerator(filtered, x => x * x);
const underHundred = takeWhileGenerator(squared, x => x < 100);
const chunked = chunkGenerator(underHundred, 3);
yield* chunked;
}
const data = [-2, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11];
const processed = [...processNumbers(data)];
console.log(processed); // [[1, 4, 9], [16, 25, 36], [49, 64, 81]]
Example 3: Asynchronous Generator Pattern
// Simulating async operations with generators
function* asyncTaskSimulator() {
console.log("Starting async task simulation...");
// Simulate API call 1
yield delay(1000).then(() => "First API response");
// Simulate API call 2
yield delay(500).then(() => "Second API response");
// Simulate processing
yield delay(800).then(() => "Processing complete");
return "All tasks finished!";
}
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// Function to run async generator
async function runAsyncGenerator(gen) {
let result = gen.next();
while (!result.done) {
try {
const value = await result.value;
console.log("Resolved:", value);
result = gen.next(value);
} catch (error) {
console.error("Error:", error);
result = gen.throw(error);
}
}
console.log("Final result:", result.value);
return result.value;
}
// Usage
const asyncGen = asyncTaskSimulator();
// runAsyncGenerator(asyncGen);
// Generator composition using yield*
function* nums() {
yield 1;
yield 2;
yield 3;
}
function* letters() {
yield 'a';
yield 'b';
yield 'c';
}
function* symbols() {
yield '!';
yield '@';
yield '#';
}
function* combined() {
yield* nums(); // Delegate to nums generator
yield* letters(); // Delegate to letters generator
yield* symbols(); // Delegate to symbols generator
}
console.log([...combined()]); // [1, 2, 3, 'a', 'b', 'c', '!', '@', '#']
// Advanced composition with transformation
function* transformAndCombine() {
// Transform numbers
function* doubledNums() {
for (let num of nums()) {
yield num * 2;
}
}
// Transform letters
function* uppercaseLetters() {
for (let letter of letters()) {
yield letter.toUpperCase();
}
}
yield* doubledNums();
yield "-separator-";
yield* uppercaseLetters();
}
console.log([...transformAndCombine()]); // [2, 4, 6, "-separator-", "A", "B", "C"]
// Traffic light state machine
function* trafficLightStateMachine() {
while (true) {
console.log("🔴 RED - Stop");
yield "red";
console.log("🟢 GREEN - Go");
yield "green";
console.log("🟡 YELLOW - Caution");
yield "yellow";
}
}
// Controlled state machine execution
class TrafficLight {
constructor() {
this.stateGenerator = trafficLightStateMachine();
this.currentState = "red";
this.timers = new Map();
}
nextState() {
const { value } = this.stateGenerator.next();
this.currentState = value;
return value;
}
start() {
const durations = { red: 3000, green: 5000, yellow: 2000 };
const cycle = () => {
this.nextState();
const duration = durations[this.currentState];
setTimeout(cycle, duration);
};
cycle();
}
getCurrentState() {
return this.currentState;
}
}
// Usage
const trafficLight = new TrafficLight();
// trafficLight.start(); // Starts the automatic cycling
// Task scheduler using generators
class TaskScheduler {
constructor() {
this.tasks = [];
this.running = false;
}
addTask(taskGenerator) {
this.tasks.push(taskGenerator);
}
start() {
if (this.running) return;
this.running = true;
this.runTasks();
}
stop() {
this.running = false;
}
runTasks() {
if (!this.running || this.tasks.length === 0) return;
// Give each task a chance to run
for (let i = this.tasks.length - 1; i >= 0; i--) {
const task = this.tasks[i];
const { done } = task.next();
// Remove completed tasks
if (done) {
this.tasks.splice(i, 1);
}
}
// Schedule next round
setTimeout(() => this.runTasks(), 10);
}
}
// Example tasks
function* countingTask(name, max) {
for (let i = 1; i <= max; i++) {
console.log(`${name}: ${i}`);
yield; // Yield control back to scheduler
}
console.log(`${name}: completed!`);
}
function* timerTask(name, seconds) {
const start = Date.now();
while (Date.now() - start < seconds * 1000) {
const elapsed = Math.floor((Date.now() - start) / 1000);
console.log(`${name}: ${elapsed}s elapsed`);
yield;
}
console.log(`${name}: timer finished!`);
}
// Usage
const scheduler = new TaskScheduler();
scheduler.addTask(countingTask("Counter1", 5));
scheduler.addTask(countingTask("Counter2", 3));
scheduler.addTask(timerTask("Timer", 2));
// scheduler.start();
// Memory-efficient data processing with generators
function* readLargeFile(filename) {
// Simulate reading a large file line by line
const lines = [
"Line 1: Lorem ipsum dolor sit amet",
"Line 2: consectetur adipiscing elit",
"Line 3: sed do eiusmod tempor incididunt",
"Line 4: ut labore et dolore magna aliqua",
"Line 5: Ut enim ad minim veniam",
// ... potentially millions of lines
];
for (let line of lines) {
yield line;
}
}
function* processLines(lineGenerator) {
for (let line of lineGenerator) {
// Process each line individually
if (line.includes("Lorem")) {
yield line.toUpperCase();
} else if (line.includes("consectetur")) {
yield line.split("").reverse().join("");
} else {
yield line;
}
}
}
// Memory efficient: only one line in memory at a time
function processLargeFileEfficiently(filename) {
const results = [];
const lineReader = readLargeFile(filename);
const processor = processLines(lineReader);
// Process first 3 lines only (early termination)
for (let i = 0; i < 3; i++) {
const { value, done } = processor.next();
if (done) break;
results.push(value);
}
return results;
}
const processed = processLargeFileEfficiently("large-file.txt");
console.log(processed);
// [
// "LINE 1: LOREM IPSUM DOLOR SIT AMET",
// "tile gnicsipida rutetcesnoc :2 eniL",
// "Line 3: sed do eiusmod tempor incididunt"
// ]
// Performance comparison: Generator vs Array
function arrayApproach(n) {
// Creates entire array in memory
const numbers = [];
for (let i = 0; i < n; i++) {
numbers.push(i * i);
}
return numbers
.filter(x => x % 2 === 0)
.map(x => x / 2)
.slice(0, 10);
}
function* generatorApproach(n) {
for (let i = 0; i < n; i++) {
const square = i * i;
if (square % 2 === 0) {
yield square / 2;
}
}
}
function generatorWithEarlyTermination(n) {
const result = [];
const gen = generatorApproach(n);
for (let value of gen) {
result.push(value);
if (result.length >= 10) break; // Early termination
}
return result;
}
// Performance test
console.time("Array approach");
const arrayResult = arrayApproach(1000000);
console.timeEnd("Array approach");
console.time("Generator approach");
const genResult = generatorWithEarlyTermination(1000000);
console.timeEnd("Generator approach");
console.log("Results match:", JSON.stringify(arrayResult) === JSON.stringify(genResult));
Application 1: Pagination System
class PaginatedDataSource {
constructor(apiUrl, pageSize = 10) {
this.apiUrl = apiUrl;
this.pageSize = pageSize;
}
// Generator for paginated data
async* fetchPages() {
let page = 1;
let hasMore = true;
while (hasMore) {
try {
const response = await this.fetchPage(page);
if (response.data.length === 0) {
hasMore = false;
} else {
yield {
page,
data: response.data,
total: response.total,
hasNext: response.hasNext
};
hasMore = response.hasNext;
page++;
}
} catch (error) {
console.error(`Error fetching page ${page}:`, error);
hasMore = false;
}
}
}
async fetchPage(page) {
// Simulate API call
return new Promise(resolve => {
setTimeout(() => {
const start = (page - 1) * this.pageSize;
const data = Array.from({ length: this.pageSize }, (_, i) => ({
id: start + i + 1,
name: `Item ${start + i + 1}`
}));
resolve({
data: page <= 5 ? data : [], // Simulate 5 pages of data
total: 50,
hasNext: page < 5
});
}, 100);
});
}
}
// Usage
async function demonstratePagination() {
const dataSource = new PaginatedDataSource("/api/items", 10);
for await (let page of dataSource.fetchPages()) {
console.log(`Page ${page.page}:`, page.data.length, "items");
// Process only first 3 pages
if (page.page >= 3) break;
}
}
// demonstratePagination();
Application 2: Event Stream Processing
class EventStream {
constructor() {
this.listeners = new Set();
}
// Generator for event stream
*events() {
const eventQueue = [];
let resolveNext = null;
// Setup event listener
const listener = (event) => {
if (resolveNext) {
resolveNext(event);
resolveNext = null;
} else {
eventQueue.push(event);
}
};
this.listeners.add(listener);
try {
while (true) {
if (eventQueue.length > 0) {
yield eventQueue.shift();
} else {
// Wait for next event
yield new Promise(resolve => {
resolveNext = resolve;
});
}
}
} finally {
this.listeners.delete(listener);
}
}
emit(eventType, data) {
const event = { type: eventType, data, timestamp: Date.now() };
for (let listener of this.listeners) {
listener(event);
}
}
// Filter events by type
*filterEvents(eventType) {
for (let event of this.events()) {
if (event.type === eventType) {
yield event;
}
}
}
// Transform events
*mapEvents(transformer) {
for (let event of this.events()) {
yield transformer(event);
}
}
}
// Usage example
const eventStream = new EventStream();
// Background process emitting events
setTimeout(() => eventStream.emit("user-action", { action: "click", target: "button1" }), 100);
setTimeout(() => eventStream.emit("user-action", { action: "scroll", position: 100 }), 200);
setTimeout(() => eventStream.emit("system-event", { type: "memory-warning" }), 300);
setTimeout(() => eventStream.emit("user-action", { action: "click", target: "button2" }), 400);
// Process user actions only
function processUserActions() {
const userActions = eventStream.filterEvents("user-action");
let processedCount = 0;
for (let event of userActions) {
console.log("User action:", event.data);
processedCount++;
if (processedCount >= 3) break; // Process first 3 actions
}
}
// processUserActions();
Generators fundamentally changed how I think about data processing and control flow. The realization that functions can be pausable and resumable opened up entirely new architectural patterns.
The key breakthrough was understanding that generators aren't just "fancy loops" - they're a powerful abstraction for lazy evaluation and cooperative control flow. They enable elegant solutions to problems that would be complex or memory-intensive with traditional approaches.
Generators taught me that the most powerful abstractions often hide complexity while exposing simple, composable interfaces.
Now that you've mastered iterators and generators, we'll explore Modules (import/export) & Dynamic Imports - the modern system for organizing and loading JavaScript code that replaced the chaos of global variables and script tags.
Remember: Iterators and generators aren't just syntax features - they're powerful tools for building memory-efficient, elegant solutions to complex data processing challenges! 🚀✨
I'm Rahul, Sr. Software Engineer (SDE II) and passionate content creator. Sharing my expertise in software development to assist learners.
More about me