Advanced JavaScript Iterators Complete Guide
Master the JavaScript iterator protocol for custom collection traversal. Covers Symbol.iterator, iterator result protocol, iterable consumers, lazy iterators, stateful iteration, bidirectional iterators, composable iterator utilities, and integration with destructuring, spread, and for-of.
The iterator protocol defines a standard way to produce a sequence of values. Any object implementing [Symbol.iterator]() becomes iterable, unlocking for...of, spread syntax, destructuring, Array.from(), and all collection-consuming APIs.
For generator-based iterators, see JavaScript Generators Deep Dive Full Guide.
The Iterator Protocol in Depth
// An ITERATOR is any object with a next() method returning { value, done }
// An ITERABLE is any object with a [Symbol.iterator]() method that returns an iterator
// Manual iterator (no generators)
class RangeIterator {
#current;
#end;
#step;
constructor(start, end, step = 1) {
this.#current = start;
this.#end = end;
this.#step = step;
}
next() {
if (this.#current < this.#end) {
const value = this.#current;
this.#current += this.#step;
return { value, done: false };
}
return { value: undefined, done: true };
}
// An iterator can be its own iterable
[Symbol.iterator]() {
return this;
}
}
const range = new RangeIterator(0, 5);
for (const n of range) {
console.log(n); // 0, 1, 2, 3, 4
}
// ITERABLE CLASS (returns fresh iterator each time)
class Range {
#start;
#end;
#step;
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 (current < end) {
const value = current;
current += step;
return { value, done: false };
}
return { value: undefined, done: true };
}
};
}
get length() {
return Math.ceil((this.#end - this.#start) / this.#step);
}
}
const r = new Range(0, 10, 2);
console.log([...r]); // [0, 2, 4, 6, 8]
console.log([...r]); // [0, 2, 4, 6, 8] -- works again because new iterator each time
// ALL BUILT-IN CONSUMERS OF THE ITERABLE PROTOCOL
const nums = new Range(1, 4);
// for...of
for (const n of nums) { /* 1, 2, 3 */ }
// Spread
const arr = [...nums]; // [1, 2, 3]
// Destructuring
const [a, b] = nums; // a=1, b=2
// Array.from
const fromArr = Array.from(nums); // [1, 2, 3]
// Map, Set constructors
const s = new Set(nums); // Set {1, 2, 3}
const m = new Map([[1,"a"],[2,"b"]]); // from iterable of [key, value]
// Promise.all, Promise.race
// yield* (in generators)
// new Int32Array(nums), new Uint8Array(nums) etc.Stateful and Closeable Iterators
// RETURN METHOD: cleanup when iteration is interrupted
class FileLineIterator {
#lines;
#index = 0;
#closed = false;
#filename;
constructor(filename, lines) {
this.#filename = filename;
this.#lines = lines;
console.log(`Opened: ${filename}`);
}
next() {
if (this.#closed || this.#index >= this.#lines.length) {
this.#cleanup();
return { value: undefined, done: true };
}
return { value: this.#lines[this.#index++], done: false };
}
// Called by for...of on break/return/throw
return(value) {
this.#cleanup();
return { value, done: true };
}
#cleanup() {
if (!this.#closed) {
this.#closed = true;
console.log(`Closed: ${this.#filename}`);
}
}
[Symbol.iterator]() {
return this;
}
}
const fileIter = new FileLineIterator("data.csv", [
"name,age", "Alice,30", "Bob,25", "Charlie,35"
]);
for (const line of fileIter) {
console.log(line);
if (line.startsWith("Bob")) break; // Triggers return()
}
// Output: Opened: data.csv, name,age, Alice,30, Bob,25, Closed: data.csv
// STATEFUL ITERATOR WITH PEEK AND PUSHBACK
class PeekableIterator {
#source;
#buffer = [];
#done = false;
constructor(iterable) {
this.#source = iterable[Symbol.iterator]();
}
next() {
if (this.#buffer.length > 0) {
return { value: this.#buffer.shift(), done: false };
}
if (this.#done) {
return { value: undefined, done: true };
}
const result = this.#source.next();
if (result.done) {
this.#done = true;
}
return result;
}
peek() {
if (this.#buffer.length > 0) {
return { value: this.#buffer[0], done: false };
}
const result = this.#source.next();
if (result.done) {
this.#done = true;
return result;
}
this.#buffer.push(result.value);
return { value: result.value, done: false };
}
pushback(value) {
this.#buffer.unshift(value);
this.#done = false;
}
[Symbol.iterator]() {
return this;
}
}
const peekable = new PeekableIterator([10, 20, 30]);
console.log(peekable.peek()); // { value: 10, done: false }
console.log(peekable.peek()); // { value: 10, done: false } (still same)
console.log(peekable.next()); // { value: 10, done: false } (consumed)
console.log(peekable.next()); // { value: 20, done: false }
peekable.pushback(15);
console.log(peekable.next()); // { value: 15, done: false } (pushed back)
console.log(peekable.next()); // { value: 30, done: false }Composable Iterator Utilities
// Functional iterator utilities that work with any iterable
const Iter = {
// Transform each element
map(iterable, fn) {
return {
[Symbol.iterator]() {
const iter = iterable[Symbol.iterator]();
return {
next() {
const { value, done } = iter.next();
return done ? { value: undefined, done } : { value: fn(value), done: false };
}
};
}
};
},
// Keep elements matching predicate
filter(iterable, predicate) {
return {
[Symbol.iterator]() {
const iter = iterable[Symbol.iterator]();
return {
next() {
while (true) {
const { value, done } = iter.next();
if (done) return { value: undefined, done: true };
if (predicate(value)) return { value, done: false };
}
}
};
}
};
},
// Take first N elements
take(iterable, n) {
return {
[Symbol.iterator]() {
const iter = iterable[Symbol.iterator]();
let count = 0;
return {
next() {
if (count >= n) return { value: undefined, done: true };
count++;
return iter.next();
}
};
}
};
},
// Concatenate multiple iterables
concat(...iterables) {
return {
[Symbol.iterator]() {
let index = 0;
let current = iterables[0]?.[Symbol.iterator]();
return {
next() {
while (current) {
const result = current.next();
if (!result.done) return result;
index++;
current = iterables[index]?.[Symbol.iterator]();
}
return { value: undefined, done: true };
}
};
}
};
},
// Zip multiple iterables into tuples
zip(...iterables) {
return {
[Symbol.iterator]() {
const iters = iterables.map(it => it[Symbol.iterator]());
return {
next() {
const results = iters.map(it => it.next());
if (results.some(r => r.done)) {
return { value: undefined, done: true };
}
return { value: results.map(r => r.value), done: false };
}
};
}
};
},
// Flatten one level
flat(iterable) {
return {
[Symbol.iterator]() {
const outer = iterable[Symbol.iterator]();
let inner = null;
return {
next() {
while (true) {
if (inner) {
const result = inner.next();
if (!result.done) return result;
inner = null;
}
const outerResult = outer.next();
if (outerResult.done) return { value: undefined, done: true };
if (outerResult.value?.[Symbol.iterator]) {
inner = outerResult.value[Symbol.iterator]();
} else {
return { value: outerResult.value, done: false };
}
}
}
};
}
};
},
// Reduce to single value
reduce(iterable, fn, initial) {
let acc = initial;
for (const item of iterable) {
acc = fn(acc, item);
}
return acc;
},
// Collect into array
toArray(iterable) {
return [...iterable];
}
};
// Composing utilities
const data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const result = Iter.toArray(
Iter.take(
Iter.map(
Iter.filter(data, n => n % 2 === 0),
n => n * n
),
3
)
);
console.log(result); // [4, 16, 36]Async Iterator Protocol
// ASYNC ITERABLES use Symbol.asyncIterator and return promises from next()
class AsyncStream {
#items;
#delayMs;
constructor(items, delayMs = 100) {
this.#items = items;
this.#delayMs = delayMs;
}
[Symbol.asyncIterator]() {
let index = 0;
const items = this.#items;
const delayMs = this.#delayMs;
return {
async next() {
if (index >= items.length) {
return { value: undefined, done: true };
}
await new Promise(r => setTimeout(r, delayMs));
return { value: items[index++], done: false };
},
async return() {
console.log("Async iterator cleanup");
return { value: undefined, done: true };
}
};
}
}
async function consumeStream() {
const stream = new AsyncStream(["alpha", "beta", "gamma"]);
for await (const item of stream) {
console.log(item);
}
}
// PAGINATED API ITERATOR
class PaginatedAPI {
#baseUrl;
#pageSize;
constructor(baseUrl, pageSize = 20) {
this.#baseUrl = baseUrl;
this.#pageSize = pageSize;
}
[Symbol.asyncIterator]() {
let page = 1;
let exhausted = false;
const baseUrl = this.#baseUrl;
const pageSize = this.#pageSize;
return {
async next() {
if (exhausted) {
return { value: undefined, done: true };
}
// Simulated API call
const response = await simulateAPI(baseUrl, page, pageSize);
if (response.items.length < pageSize) {
exhausted = true;
}
page++;
return { value: response.items, done: false };
}
};
}
}
async function simulateAPI(url, page, size) {
const totalItems = 45;
const start = (page - 1) * size;
const items = [];
for (let i = start; i < Math.min(start + size, totalItems); i++) {
items.push({ id: i + 1, name: `Item ${i + 1}` });
}
return { items, page, totalPages: Math.ceil(totalItems / size) };
}
// Consume all pages
async function getAllItems() {
const api = new PaginatedAPI("/api/items", 20);
const allItems = [];
for await (const page of api) {
allItems.push(...page);
console.log(`Fetched ${allItems.length} items`);
if (page.length === 0) break;
}
return allItems;
}| Protocol | Method | Returns | Consumer | Use Case |
|---|---|---|---|---|
| Iterator | next() | { value, done } | for...of, spread | Synchronous sequences |
| Async Iterator | async next() | Promise<{ value, done }> | for await...of | Async data streams |
| Iterable | [Symbol.iterator]() | Iterator | All sync consumers | Custom collections |
| Async Iterable | [Symbol.asyncIterator]() | Async Iterator | Async consumers | Paginated APIs, streams |
Rune AI
Key Insights
- The iterable protocol requires Symbol.iterator returning an iterator; the iterator protocol requires next() returning { value, done }: Separating these lets iterables produce fresh iterators for repeated traversal
- Implement return() on iterators that hold resources to ensure cleanup when consumers exit early via break, return, or throw: Without return(), resources leak on incomplete iteration
- Composable iterator utilities (map, filter, take, zip, flat) enable lazy pipelines without intermediate arrays: Each utility wraps an iterator and transforms values on demand
- The async iterator protocol mirrors the sync version but next() returns promises, consumed via for await...of: This is ideal for paginated APIs, real-time streams, and network data
- PeekableIterator with pushback enables look-ahead parsing and tokenization that standard iterators cannot express: Buffer consumed values and re-emit them for subsequent consumers
Frequently Asked Questions
What is the difference between an iterator and an iterable?
When should I implement return() on my iterator?
How do iterator helpers (TC39 proposal) change iteration?
Can I make any object iterable?
Conclusion
The iterator protocol provides a universal interface for sequential data access. Custom iterators, composable utilities, and the async iterator protocol enable elegant handling of collections, streams, and lazy computation. For building custom iterable data structures, continue to Creating JavaScript Custom Iterables Full Guide. For async flows built on these protocols, see Handling Async Flows with JS Generator Functions.
More in this topic
OffscreenCanvas API in JS for UI Performance
Master the OffscreenCanvas API to offload rendering from the main thread. Covers worker-based 2D and WebGL rendering, animation loops inside workers, bitmap transfer, double buffering, chart rendering pipelines, image processing, and performance measurement strategies.
Advanced Web Workers for High Performance JS
Master Web Workers for truly parallel JavaScript execution. Covers dedicated and shared workers, structured cloning, transferable objects, SharedArrayBuffer with Atomics, worker pools, task scheduling, Comlink RPC patterns, module workers, and performance profiling strategies.
JavaScript Macros and Abstract Code Generation
Master JavaScript code generation techniques for compile-time and runtime metaprogramming. Covers AST manipulation, Babel plugin authorship, tagged template literals as macros, code generation pipelines, source-to-source transformation, compile-time evaluation, and safe eval alternatives.