Iterator
The Iterator pattern exposes elements of a collection one at a time without revealing the underlying structure. In modern languages it’s baked into the standard library — for...of, generators, IEnumerable, Iterator trait — so you usually implement it rather than recreate the dispatch.
Iterator protocol, generators, ranges
EXAMPLE
// 1) The Iterator protocol (JavaScript)
// An object with a next() method that returns { value, done }.
const iter = {
i: 0,
next() {
if (this.i < 3) return { value: this.i++, done: false };
return { value: undefined, done: true };
},
};
console.log(iter.next()); // { value: 0, done: false }
console.log(iter.next()); // { value: 1, done: false }
console.log(iter.next()); // { value: 2, done: false }
console.log(iter.next()); // { value: undefined, done: true }
// 2) Iterable — has [Symbol.iterator]() that returns an Iterator
class Range {
constructor(start, end, step = 1) {
this.start = start; this.end = end; this.step = step;
}
[Symbol.iterator]() {
let i = this.start;
const { end, step } = this;
return {
next() {
if (i < end) return { value: i, done: false }; i += step; return arguments.length ? { value: i - step, done: false } : { value: i - step, done: false };
},
};
}
}
// Used with for…of, spread, destructuring
for (const x of new Range(0, 5)) console.log(x); // 0, 1, 2, 3, 4
const arr = [...new Range(0, 5)]; // [0,1,2,3,4]
// 3) Generators — built-in syntax for iterators
function* counter(start, end, step = 1) {
for (let i = start; i < end; i += step) yield i;
}
for (const n of counter(0, 5)) console.log(n); // 0..4
// Generators ARE iterators AND iterables.
[...counter(0, 3)]; // [0, 1, 2]
// 4) Infinite iterators with take()
function* naturals() {
let i = 0;
while (true) yield i++;
}
function* take(it, n) {
let count = 0;
for (const x of it) {
if (count++ >= n) break;
yield x;
}
}
[...take(naturals(), 5)]; // [0, 1, 2, 3, 4]
// 5) Composable iterator helpers (built-in in TC39 Iterator Helpers / Array methods)
naturals()
.map((x) => x * x)
.filter((x) => x % 2 === 0)
.take(5)
.toArray();
// Browser/Node support varies; use a polyfill or library for older runtimes.
// 6) Custom iterable — Tree traversal
class TreeNode {
constructor(value) { this.value = value; this.children = []; }
add(child) { this.children.push(child); return this; }
*[Symbol.iterator]() { // depth-first
yield this.value;
for (const c of this.children) yield* c;
}
}
const root = new TreeNode('a').add(new TreeNode('b').add(new TreeNode('d'))).add(new TreeNode('c'));
for (const v of root) console.log(v); // a, b, d, c
// 7) Async iterators — for await…of
async function* paginate(url) {
let next = url;
while (next) {
const r = await fetch(next).then((r) => r.json());
for (const item of r.items) yield item;
next = r.nextUrl;
}
}
for await (const user of paginate('/api/users?page=1')) {
console.log(user);
}
// Async iterators are perfect for streaming APIs, pagination, message queues.
// 8) In other languages
//
// Python — __iter__ / __next__ or 'yield'
// class Range:
// def __init__(self, start, end): self.start, self.end = start, end
// def __iter__(self): self.i = self.start; return self
// def __next__(self):
// if self.i >= self.end: raise StopIteration
// self.i += 1
// return self.i - 1
// Rust — Iterator trait
// impl Iterator for Counter { type Item = u32; fn next(&mut self) -> Option<u32> { … } }
// C# — IEnumerable<T> / IEnumerator<T> / yield return
// public IEnumerable<int> Counter() { for (int i = 0; i < 10; i++) yield return i; }
// Java — Iterator<E> / Iterable<E>
// Go — no language-level iterator; use channels or range over a slice
// (Go 1.23+ has range-over-function with iter package)
// 9) When iterator is the right pattern
// • Walk a collection without exposing internal structure (trees, graphs, custom containers)
// • Stream data lazily — avoids materialising huge sequences
// • Compose pipelines without intermediate arrays
// • Pause / resume via generators (state machines, coroutines)
// 10) When NOT to bother
// • Plain arrays + map/filter/forEach are usually enough
// • Simple loops over fixed-size data — direct for(let i = 0; i < n; i++) is fine
// 11) Streams vs iterators
// • Streams (Node Readable, RxJS Observable) — push model; producer decides
// • Iterators — pull model; consumer pulls next()
// • Async iterators bridge both: consumer pulls, but the value arrives asynchronously
// 12) Combine with state machines
function* stateMachine(events) {
let state = 'idle';
while (true) {
const event = yield state;
switch (state) {
case 'idle': if (event === 'start') state = 'running'; break;
case 'running': if (event === 'stop') state = 'idle'; break;
}
}
}
const sm = stateMachine();
sm.next(); // start
sm.next('start'); // 'running'
sm.next('stop'); // 'idle'
// 13) Common bugs
// • Returning the same iterator instance from [Symbol.iterator]() — can't loop twice; return a NEW one
// • Mutating the underlying collection while iterating — undefined behaviour; copy or use defensive copies
// • Generator with leaked resources — use try/finally inside the generator body to clean up
// • Async generator without cancellation — pulls forever; pair with AbortSignal
// • for…of on an Object — Objects aren't iterable; use Object.entries / keys / values
// • Returning { done: false } with no value — confuses callers; use { value: undefined, done: true } when done
// • Spread of infinite iterator — stack overflow; cap with take()
// • Calling .next() after done: true — should keep returning done; some custom iterators throw
Why it matters
Reach for the iterator pattern when traversal logic shouldn’t leak the underlying structure — trees, graphs, custom containers, infinite sequences, streaming pagination. Use generators (function*) for synchronous iteration and async function* + for await…of for streams. Compose with map/filter/take for lazy pipelines.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
// JS uses Symbol.iterator.
class Range {
constructor(start, end) { this.start = start; this.end = end; }
*[Symbol.iterator]() {
for (let i = this.start; i < this.end; i++) yield i;
}
}
for (const n of new Range(0, 5)) console.log(n);
Try it Yourself »
Discussion
Loading…