Iterator pattern and its use cases
The Iterator Pattern is a design pattern that lets us access elements of a collection one at a time, without knowing or exposing how the collection is structured. We will implement iterator in Javascript using iterables and iterators.
- Iterable: Any object that can be looped over with
for...of
. It implements a method called[Symbol.iterator]
. - Iterator: An object returned by the iterable’s
[Symbol.iterator]()
method. It has anext()
method that returns{ value, done }
. In this article we are going to look at the iterator pattern and dive deep into areas we can apply it which include; - Middleware pipelines to traverse requests.
- Streaming data to process logs, server-sent events, or infinite sequences.
- UI trees to traverse complex DOM or component trees efficiently.
- Lazy evaluation: handle large datasets or infinite sequences without memory spikes.
1. Simple iterator in Javascript
In this example we use iterator pattern lets you go through a collection one by one without worrying about its underlying structure.
const numbers = [10, 20, 30];
// Get the iterator
const iterator = numbers[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 }
2. Custom Iterator
Here we will be creating an infinite sequence to demonstrate how the Iterator Pattern goes beyond arrays and lists, it can generate values on demand without ever storing them in memory. Each call to next()
simply produces the next number, making the sequence effectively unbounded. This matters in real-world scenarios where you’re dealing with streams, live feeds, or massive datasets, because you don’t need to load everything upfront. Instead, you can pull values one at a time as you need them, which keeps your applications efficient, scalable, and capable of handling data that would otherwise be too large or endless to manage directly.
// Infinite sequence generator
const infiniteSequence = {
[Symbol.iterator]() {
let n = 0;
return {
next() {
n++;
return { value: n, done: false };
}
};
}
};
const iterator = infiniteSequence[Symbol.iterator]();
console.log(iterator.next()); // { value: 1, done: false }
console.log(iterator.next()); // { value: 2, done: false }
3. Async Iterators since data is not always ready instantly
To use async iterators we will make use of generator functions so we will first look at what a generator function is;
A generator function in JavaScript is a special kind of function that can pause and resume its execution. Instead of running from start to finish in one go, it produces values one at a time using the yield keyword we define generator using function* as shown below.
// A generator function that yields numbers from 1 to 3
function* numberGenerator() {
yield 1; // pause and give back 1
yield 2; // pause and give back 2
yield 3; // pause and give back 3
}
// Create a generator object (iterator)
const gen = numberGenerator();
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 }
In the above function*
creates a generator function, calling it doesn’t run everything immediately — it returns a generator object (which is both an iterator and iterable). We step through it with next()
each yield pauses execution and returns a value and when no more yields are left, it returns { done: true }
.
Let us now use genertor in our async iterator In the code below, we define an async
generator function fetchPages()
that fetches three pages of data from an API one at a time; instead of returning everything at once, it uses yield to pass each page’s items back to the caller as soon as they’re ready. The for await...of
loop consumes this async generator by automatically awaiting each yielded value, logging page results as they stream in. This pattern lets you process data lazily and asynchronously, which is more efficient than waiting for all pages before starting work.
// An async generator function that fetches 3 pages of data one by one
async function* fetchPages() {
let page = 1;
while (page <= 3) {
// Fetch data from an API and wait for the response
const data = await fetch(`https://api.example.com/page/${page}`).then(r => r.json());
// Yield the current page's items to the consumer
yield data.items;
page++;
}
}
// Consume the async generator: each iteration waits for the next page
for await (const items of fetchPages()) {
console.log(items); // logs each page's items as they arrive
}
4. Iterator + functional programming can help us filter or transform many items without ever holding all results in memory.
In the code below we define two generator-based functions, selectItems
and transformItems
, which together implement a lazy evaluation pipeline similar to array methods but without creating intermediate arrays. selectItems
iterates over a sequence (seq)
and yields
only the items that match a given condition, while transformItems
takes a sequence and yields each item after applying a transformation function. In the example, the pipeline first selects odd numbers from the array [1, 2, 3, 4, 5]
, producing 1, 3, 5,
and then transforms them by multiplying each by 10, resulting in [10, 30, 50]
. Unlike native array methods, this generator-based approach is lazy—items are produced one at a time only when needed, making it far more efficient for large or even infinite sequences.
// A generator that selects only items matching a condition
function* selectItems(seq, condition) {
for (const item of seq) {
if (condition(item)) yield item; // yield only if condition passes
}
}
// A generator that transforms each item before yielding
function* transformItems(seq, transformFn) {
for (const item of seq) yield transformFn(item); // apply transform and yield
}
const numbers = [1, 2, 3, 4, 5];
// Pipeline: select odd numbers, then multiply them by 10
const result = transformItems(
selectItems(numbers, n => n % 2 === 1), // yields 1, 3, 5
n => n * 10 // transforms to 10, 30, 50
);
// Spread into array to evaluate the generator
console.log([...result]); // [10, 30, 50]
5. Iterator in middleware pipelines to traversing requests
The code below defines a MiddlewarePipeline
class that models how middleware systems (like Express.js) work internally. It allows you to register multiple middleware functions using .use()
, and then executes them in sequence by making the class iterable with [Symbol.iterator]
. Each middleware receives a context object (such as request data) and a next function to explicitly call the following middleware in the chain. The iterator ensures middlewares are executed in order, abstracting away the traversal logic. In the example, two middlewares are added—one for authentication and one for logging—and the pipeline is iterated, running each middleware in turn with access to the shared ctx object.
// Middleware chain iterator implementation
class MiddlewarePipeline {
constructor() {
this.middlewares = []; // store middleware functions
}
// Register a new middleware function
use(fn) {
this.middlewares.push(fn);
}
// Make the pipeline iterable using the Iterator Pattern
[Symbol.iterator]() {
let index = 0;
const middlewares = this.middlewares;
return {
// The "next" method drives the iteration over middlewares
next(context) {
if (index < middlewares.length) {
const fn = middlewares[index++];
// Execute the current middleware and pass "next" so it can call the next one
fn(context, () => this.next(context));
return { value: fn, done: false };
}
// End of middleware chain
return { done: true };
}
};
}
}
// how its consumed
// Create a pipeline
const pipeline = new MiddlewarePipeline();
// Add middleware: authentication check
pipeline.use((ctx, next) => {
console.log('Auth check for:', ctx.user);
next(); // move to next middleware
});
// Add middleware: logging
pipeline.use((ctx, next) => {
console.log('Logging action for:', ctx.user);
next();
});
// Context object, similar to a request in Express
const ctx = { user: 'Koome' };
// Iterate over the pipeline and execute each middleware
for (const mw of pipeline) {
mw(ctx, () => {}); // manually provide a final "next" as a placeholder
}
6. Streaming data – logs or server-sent events
Below we use the iterator pattern with asynchronous generators to simulate streaming data. The logStream function
is defined as an async generator
that yields log entries one by one, pausing between each with a setTimeout
to mimic data arriving over time (like logs or server events). Instead of loading all logs at once, the consumer processes each entry as soon as it becomes available using for await...of
, making the solution scalable and memory-efficient. This pattern is especially powerful for handling real-time data streams such as logs, messages, or API events.
// Simulated log stream using an async generator
async function* logStream() {
// Pretend these are incoming log messages
const logs = ["User login", "File uploaded", "User logout"];
// Yield logs one at a time with a delay to simulate streaming
for (const log of logs) {
await new Promise(r => setTimeout(r, 500)); // simulate async data arrival
yield log; // yield the log entry to the consumer
}
}
// Consume logs lazily as they arrive
(async () => {
// "for await...of" allows us to consume async iterators step by step
for await (const entry of logStream()) {
console.log("Processing log:", entry);
}
})();
7. Composable UI trees – traversing DOM or component trees
This code below defines a generator function traverseDOM
that performs a depth-first traversal of a DOM tree. Starting from a given root node
, it yields the node itself and then recursively yields all of its children, regardless of how deeply nested they are. This approach allows you to iterate through the entire DOM structure in sequence without manually writing nested loops. In the example, a simple tree with <div>, <p>, <span>, and <b>
elements is created. The for...of
loop consumes the generator, printing out each element’s tagName in the order they are visited. This demonstrates how the Iterator Pattern can be applied to traverse hierarchical structures like the DOM in a clean, reusable way.
// A generator that walks through the DOM tree depth-first
function* traverseDOM(node) {
yield node; // yield the current node first
for (const child of node.children) {
// Recursively yield all children of the current node
yield* traverseDOM(child);
}
}
// --- Example usage ---
// Create a root DOM node dynamically
const treeRoot = document.createElement('div');
// Add some nested elements inside the root
treeRoot.innerHTML = `
<p>Text</p>
<span><b>Bold</b></span>
`;
// Traverse the DOM tree starting from treeRoot
for (const el of traverseDOM(treeRoot)) {
// Print the tag name of each visited node
console.log("Visited:", el.tagName);
}
8. Lazy Evaluation useful for generating large datasets or infinite sequences on demand.
This code below demonstrates how to build an infinite, lazy-evaluated data pipeline using generator functions. The infiniteNumbers generator
produces an endless sequence of natural numbers starting from 1, yielding the next value each time it’s called. To process this infinite sequence efficiently, the filter generator yields only values that match a condition (e.g., even numbers), and the map generator transforms those values on demand (e.g., multiplying by 10). Because the iteration is lazy, numbers are only generated, filtered, and transformed when requested, which allows handling potentially infinite sequences without memory issues. In the example, the program requests the first five even numbers multiplied by 10, producing [20, 40, 60, 80, 100]
.
// Infinite sequence generator: produces numbers starting from 1 endlessly
function* infiniteNumbers() {
let n = 1;
while (true) {
yield n++; // yield current number, then increment
}
}
// Custom filter generator: yields only items that match a given condition
function* filter(seq, predicate) {
for (const item of seq) {
if (predicate(item)) yield item; // yield only if condition returns true
}
}
// Custom map generator: applies a transformation function to each item lazily
function* map(seq, fn) {
for (const item of seq) {
yield fn(item); // apply function and yield transformed value
}
}
// --- Usage Example ---
// Start infinite sequence
const numbers = infiniteNumbers();
// Create pipeline: select even numbers, then multiply by 10
const processed = map(
filter(numbers, n => n % 2 === 0), // filter out odd numbers
n => n * 10 // transform each to n*10
);
// Request the first 5 values from the pipeline
// Each call to .next() triggers the next step of the lazy sequence
console.log([...Array(5)].map(() => processed.next().value));
// Output: [20, 40, 60, 80, 100]
javascript book
If this interested you, check out my Javascript Book