|$ curl https://forge-ai.dev/api/markdown?path=docs/js/iterators
$cat docs/javascript-—-iterators-&-iterables.md
updated Recently·30 min read·published

JavaScript — Iterators & Iterables

JavaScriptIteratorsAdvancedIntermediate to Advanced
Introduction

The iteration protocol is one of the most fundamental contracts in JavaScript. It defines how objects produce sequences of values and how consumers can traverse those sequences uniformly. Understanding iteration unlocks the full power of for...of, spread syntax, destructuring, and many built-in APIs.

The protocol consists of two core concepts: iterables (objects that can produce a sequence of values) and iterators (objects that manage the traversal). An iterable has a [Symbol.iterator]() method that returns an iterator. The iterator has a next() method that returns { value, done } objects.

This protocol is the backbone of JavaScript's iteration ecosystem. Arrays, Strings, Maps, Sets, TypedArrays, NodeLists, and even the arguments object are iterable. Generators produce iterators. Custom objects can opt into iteration by implementing the protocol. Once something is iterable, it works with all language features designed for iteration.

iter-intro.js
JavaScript
1// The iteration protocol at a glance
2const array = [10, 20, 30];
3
4// Array is iterable — it has Symbol.iterator
5const iterator = array[Symbol.iterator]();
6
7console.log(iterator.next()); // { value: 10, done: false }
8console.log(iterator.next()); // { value: 20, done: false }
9console.log(iterator.next()); // { value: 30, done: false }
10console.log(iterator.next()); // { value: undefined, done: true }
11
12// All of these work because arrays are iterable:
13for (const val of array) { /* ... */ }
14const copy = [...array];
15const [first, second] = array;
16const fromArray = Array.from(array);
The Iteration Protocol

The iteration protocol is actually two protocols working together: the iterable protocol and the iterator protocol. Any object that implements [Symbol.iterator]() is iterable. The method must return an iterator — an object with a next() method that returns { value: any, done: boolean }.

ProtocolInterfaceReturnsDescription
Iterable[Symbol.iterator]()IteratorFactory method that returns a new iterator each time
Iteratornext(){ value, done }Returns the next value or signals completion
Iterator (opt)return(){ value, done }Called when iteration is terminated early (break, throw)
Iterator (opt)throw(){ value, done }Called to signal an error to the iterator
protocol-detail.js
JavaScript
1// The protocol explained step by step
2// 1. An iterable is any object with [Symbol.iterator]
3const iterable = {
4 [Symbol.iterator]: function() {
5 // 2. Returns an iterator
6 let count = 0;
7 return {
8 next: function() {
9 count++;
10 if (count <= 3) {
11 return { value: count * 10, done: false };
12 }
13 return { value: undefined, done: true };
14 },
15 // Optional: called on early termination
16 return: function() {
17 console.log("Iterator cleaned up");
18 return { value: undefined, done: true };
19 }
20 };
21 }
22};
23
24for (const val of iterable) {
25 console.log(val); // 10, 20, 30
26}
27
28// Early break calls return()
29for (const val of iterable) {
30 if (val === 20) break; // triggers return()
31}

info

An iterator can also be iterable by returning this from its [Symbol.iterator]() method. This is called an iterable iterator. Generators do this automatically — the generator object is both an iterator and iterable, which is why you can use generators directly in for...of loops.
Making Objects Iterable

Any object can become iterable by implementing [Symbol.iterator](). This is one of the most practical patterns in JavaScript — it allows custom data structures to participate in the standard iteration ecosystem (for...of, spread, destructuring, Array.from).

making-iterable.js
JavaScript
1// Making a simple collection iterable
2class Team {
3 constructor(name) {
4 this.name = name;
5 this.members = [];
6 }
7
8 addMember(name) {
9 this.members.push(name);
10 return this;
11 }
12
13 // Make it iterable
14 [Symbol.iterator]() {
15 let index = 0;
16 const members = this.members;
17 return {
18 next() {
19 if (index < members.length) {
20 return { value: members[index++], done: false };
21 }
22 return { value: undefined, done: true };
23 }
24 };
25 }
26}
27
28const team = new Team("Devs");
29team.addMember("Alice").addMember("Bob").addMember("Charlie");
30
31// Now it works with all iteration features
32for (const member of team) {
33 console.log(member); // "Alice", "Bob", "Charlie"
34}
35
36console.log([...team]); // ["Alice", "Bob", "Charlie"]
37const [lead, ...rest] = team;
38console.log(lead); // "Alice"
39console.log(rest); // ["Bob", "Charlie"]
40
41// You can also make plain objects iterable
42const range = {
43 from: 1,
44 to: 5,
45 [Symbol.iterator]() {
46 let current = this.from;
47 const end = this.to;
48 return {
49 next() {
50 if (current <= end) {
51 return { value: current++, done: false };
52 }
53 return { value: undefined, done: true };
54 }
55 };
56 }
57};
58
59console.log([...range]); // [1, 2, 3, 4, 5]
iterable-generator.js
JavaScript
1// Iterable with generator (cleaner syntax)
2class PaginatedResult {
3 constructor(data, pageSize = 10) {
4 this.data = data;
5 this.pageSize = pageSize;
6 }
7
8 *[Symbol.iterator]() {
9 for (let i = 0; i < this.data.length; i += this.pageSize) {
10 yield this.data.slice(i, i + this.pageSize);
11 }
12 }
13}
14
15const allItems = Array.from({ length: 25 }, (_, i) => `item-${i}`);
16const paginated = new PaginatedResult(allItems, 10);
17
18for (const page of paginated) {
19 console.log("Page:", page);
20 // ["item-0", ..., "item-9"]
21 // ["item-10", ..., "item-19"]
22 // ["item-20", ..., "item-24"]
23}
24
25// Practical: iterable matrix
26class Matrix {
27 constructor(rows, cols, fill = 0) {
28 this.data = Array.from({ length: rows }, () =>
29 Array.from({ length: cols }, () => fill)
30 );
31 }
32
33 *[Symbol.iterator]() {
34 for (const row of this.data) {
35 yield* row;
36 }
37 }
38
39 *rows() {
40 yield* this.data;
41 }
42
43 *columns() {
44 for (let col = 0; col < this.data[0].length; col++) {
45 yield this.data.map(row => row[col]);
46 }
47 }
48}
49
50const m = new Matrix(3, 3);
51console.log([...m]); // [0, 0, 0, 0, 0, 0, 0, 0, 0]
52console.log([...m.rows()]); // [[0,0,0], [0,0,0], [0,0,0]]
53console.log([...m.columns()]); // [[0,0,0], [0,0,0], [0,0,0]]
🔥

pro tip

The combination of *[Symbol.iterator]() with generators is the most elegant way to make objects iterable. It eliminates boilerplate — no need to manage index state, return objects, or handle completion. The generator handles the protocol automatically.
Built-in Iterables

JavaScript provides many built-in iterables. Each collection type implements the iterable protocol, and they can be used interchangeably with any language feature that consumes iterables.

TypeIteratesExampleNotes
ArrayElements in index order[1, 2, 3]Most common iterable
StringCharacters (code points)"abc"Iterates by Unicode code points, not UTF-16 code units
Map[key, value] entriesnew Map()Insertion order
SetValues in insertion ordernew Set()Each value appears once
TypedArrayNumeric elementsUint8ArrayBinary data buffers
NodeListDOM nodesdocument.querySelectorAll()Iterable in modern browsers
argumentsFunction argumentsargumentsLegacy, avoid in favor of rest params
GeneratorYielded valuesfunction*() Both iterable and iterator
builtin-iterables.js
JavaScript
1// Array — iterate elements
2for (const el of [10, 20, 30]) console.log(el);
3
4// String — iterate Unicode code points
5for (const char of "hello") console.log(char);
6for (const emoji of "👍🎉") console.log(emoji); // Works correctly
7// String iterator handles surrogate pairs correctly
8console.log([..."👍🎉"]); // ["👍", "🎉"]
9
10// Map — iterate [key, value] pairs
11const map = new Map([["a", 1], ["b", 2]]);
12for (const [key, value] of map) {
13 console.log(key, value); // "a" 1, "b" 2
14}
15console.log([...map.keys()]); // ["a", "b"]
16console.log([...map.values()]); // [1, 2]
17
18// Set — iterate unique values
19const set = new Set([1, 1, 2, 3, 3]);
20for (const val of set) console.log(val); // 1, 2, 3
21console.log([...set]); // [1, 2, 3]
22
23// NodeList — iterate DOM nodes
24document.querySelectorAll("div"); // iterate with for...of
25
26// TypedArray
27const bytes = new Uint8Array([0x48, 0x65, 0x6C, 0x6C, 0x6F]);
28console.log([...bytes].map(c => String.fromCharCode(c)).join("")); // "Hello"

info

String iteration works by Unicode code points, not UTF-16 code units. This means emojis and other multi-byte characters are handled correctly: [..."👍"] yields ["👍"], not two surrogate halves. Older string methods like charAt() do not handle this correctly.
for...of Under the Hood

The for...of loop is syntactic sugar over the iteration protocol. Understanding what it does internally helps you debug iteration issues and appreciate what the protocol provides for free.

for-of-internals.js
JavaScript
1// for...of is syntactic sugar for this pattern:
2const iterable = [10, 20, 30];
3
4// What for...of does internally:
5const iterator = iterable[Symbol.iterator]();
6let result = iterator.next();
7
8while (!result.done) {
9 const value = result.value;
10 console.log(value); // 10, 20, 30
11
12 // If break is encountered, iterator.return() is called
13 // if (condition) { iterator.return?.(); break; }
14
15 result = iterator.next();
16}
17// Iterator is exhausted — all values consumed
18
19// With early termination protection
20function iterateSafely(iterable) {
21 const iterator = iterable[Symbol.iterator]();
22 try {
23 let result = iterator.next();
24 while (!result.done) {
25 // Process result.value
26 console.log(result.value);
27 result = iterator.next();
28 }
29 } finally {
30 // Ensure cleanup on early exit or error
31 if (iterator.return) iterator.return();
32 }
33}
34
35// This is why for...of calls return() when you break:
36const withCleanup = {
37 [Symbol.iterator]() {
38 let i = 0;
39 return {
40 next() { return i < 3 ? { value: i++, done: false } : { done: true }; },
41 return() { console.log("Cleanup!"); return { done: true }; }
42 };
43 }
44};
45
46for (const v of withCleanup) {
47 if (v === 1) break; // Logs "Cleanup!"
48}

best practice

The return() method on iterators is called automatically when for...of exits early (via break, continue with label, return, or throw). Use it to release resources — close file handles, abort network requests, remove event listeners. Always handle cleanup in iterators that acquire resources.
Spread Operator & Iterables

The spread operator (...) works with any iterable. It calls [Symbol.iterator]() and consumes all values into the target context. This is why spread works not just on arrays, but on strings, sets, maps, and any custom iterable.

spread-iterables.js
JavaScript
1// Spread works with any iterable
2const arr1 = [1, 2, 3];
3const arr2 = [4, 5, 6];
4const combined = [...arr1, ...arr2]; // [1, 2, 3, 4, 5, 6]
5
6// Strings are iterable — spread into characters
7const chars = [..."hello"]; // ["h", "e", "l", "l", "o"]
8const emojis = [..."🎉🚀💡"]; // ["🎉", "🚀", "💡"]
9
10// Sets are iterable — spread into array (deduplicate)
11const unique = [...new Set([1, 2, 2, 3, 3, 4])]; // [1, 2, 3, 4]
12
13// Maps spread into [key, value] pairs
14const map = new Map([["a", 1], ["b", 2]]);
15const entries = [...map]; // [["a", 1], ["b", 2]]
16const keys = [...map.keys()]; // ["a", "b"]
17
18// Works with custom iterables
19const range = {
20 from: 1, to: 5,
21 [Symbol.iterator]() {
22 let i = this.from;
23 return { next: () => i <= this.to ? { value: i++, done: false } : { done: true } };
24 }
25};
26console.log([...range]); // [1, 2, 3, 4, 5]
27
28// Spread in function calls
29function sum(...numbers) {
30 return numbers.reduce((a, b) => a + b, 0);
31}
32const nums = [1, 2, 3, 4, 5];
33console.log(sum(...nums)); // 15
34
35// Spread in object literals (own properties, NOT iteration)
36const obj1 = { x: 1, y: 2 };
37const obj2 = { ...obj1, z: 3 }; // { x: 1, y: 2, z: 3 }
38// Note: object spread uses own property enumeration, NOT iteration

info

Object spread ({ ...obj }) does not use the iteration protocol. It copies own enumerable properties using Object.keys() and property accessors. This is a common point of confusion — objects are not iterable by default, so spreading an object works differently than spreading an array.
Array.from with Iterables

Array.from() converts any iterable (or array-like object) into a real array. It is safer and more explicit than spread for some cases, especially when dealing with array-like objects that are not iterable (like the old arguments object in non-strict mode).

array-from.js
JavaScript
1// Array.from with iterables
2const set = new Set([1, 2, 3, 3, 4]);
3const arr1 = Array.from(set); // [1, 2, 3, 4]
4const arr2 = [...set]; // [1, 2, 3, 4] (same result)
5
6// Array.from accepts a map function (second argument)
7const doubled = Array.from(set, x => x * 2); // [2, 4, 6, 8]
8
9// Create arrays from range
10const range = Array.from({ length: 5 }, (_, i) => i + 1); // [1, 2, 3, 4, 5]
11
12// From string
13const chars = Array.from("hello"); // ["h", "e", "l", "l", "o"]
14
15// From NodeList (safer than spread in older environments)
16// const divs = Array.from(document.querySelectorAll("div"));
17
18// Array.from with array-like objects (not iterable)
19const arrayLike = { 0: "a", 1: "b", 2: "c", length: 3 };
20const realArray = Array.from(arrayLike); // ["a", "b", "c"]
21// Spread would fail here — array-likes are not iterable
22
23// Practical: generate sequences
24function range(start, end, step = 1) {
25 const length = Math.ceil((end - start) / step);
26 return Array.from({ length }, (_, i) => start + i * step);
27}
28console.log(range(0, 10, 2)); // [0, 2, 4, 6, 8]
29
30// Practical: grid initialization
31const grid = Array.from({ length: 3 }, () =>
32 Array.from({ length: 3 }, () => 0)
33);
34// [[0,0,0], [0,0,0], [0,0,0]]
35
36// Array.from vs spread:
37// - Array.from accepts a mapFn — one fewer iteration
38// - Array.from handles array-likes (non-iterable)
39// - Spread is more concise for simple cases
40// - Array.from is slightly faster for large iterables

best practice

Use Array.from() when you need to map values in the same step (Array.from(iterable, x => ...)), or when dealing with array-like objects like arguments. Use spread ([...iterable]) for simpler cases where clarity matters more. Both are O(n) but spread creates an intermediate array that is then spread.
Custom Iterator Patterns

Beyond making objects iterable, you can write standalone iterators that encapsulate complex traversal logic. Common patterns include infinite iterators, transforming iterators (map, filter), and iterators that manage external resources.

custom-iterators.js
JavaScript
1// Standalone iterator factory
2function createIterator(data) {
3 let index = 0;
4 return {
5 next() {
6 if (index < data.length) {
7 return { value: data[index++], done: false };
8 }
9 return { value: undefined, done: true };
10 },
11 // Make it iterable too
12 [Symbol.iterator]() { return this; }
13 };
14}
15
16const it = createIterator(["a", "b", "c"]);
17console.log(it.next().value); // "a"
18console.log(it.next().value); // "b"
19console.log(it.next().value); // "c"
20console.log(it.next().done); // true
21
22// Transforming iterator: map
23function mapIterator(iterator, fn) {
24 return {
25 next() {
26 const { value, done } = iterator.next();
27 return done ? { done } : { value: fn(value), done: false };
28 },
29 [Symbol.iterator]() { return this; }
30 };
31}
32
33const base = createIterator([1, 2, 3]);
34const doubled = mapIterator(base, x => x * 2);
35console.log([...doubled]); // [2, 4, 6]
36
37// Zip two iterators
38function zipIterator(...iterables) {
39 const iterators = iterables.map(i => i[Symbol.iterator]());
40 return {
41 next() {
42 const values = [];
43 for (const it of iterators) {
44 const result = it.next();
45 if (result.done) return { value: undefined, done: true };
46 values.push(result.value);
47 }
48 return { value: values, done: false };
49 },
50 [Symbol.iterator]() { return this; }
51 };
52}
53
54const names = ["Alice", "Bob", "Charlie"];
55const ages = [30, 25, 35];
56const zipped = zipIterator(names, ages);
57console.log([...zipped]); // [["Alice", 30], ["Bob", 25], ["Charlie", 35]]
iterator-patterns.js
JavaScript
1// Fibonacci iterator (infinite)
2function fibonacciIterator() {
3 let a = 0, b = 1;
4 return {
5 next() {
6 const value = a;
7 [a, b] = [b, a + b];
8 return { value, done: false }; // Never done!
9 },
10 [Symbol.iterator]() { return this; }
11 };
12}
13
14const fib = fibonacciIterator();
15console.log(fib.next().value); // 0
16console.log(fib.next().value); // 1
17console.log(fib.next().value); // 1
18console.log(fib.next().value); // 2
19console.log(fib.next().value); // 3
20
21// Take helper for infinite iterators
22function take(n, iterator) {
23 let count = 0;
24 return {
25 next() {
26 if (count++ >= n) return { done: true };
27 return iterator.next();
28 },
29 [Symbol.iterator]() { return this; }
30 };
31}
32
33const first10Fib = [...take(10, fibonacciIterator())];
34console.log(first10Fib); // [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
35
36// Paging iterator (external resource management)
37function pagingIterator(fetchPage, options = {}) {
38 const { maxPages = Infinity } = options;
39 let page = 0;
40 let index = 0;
41 let currentPage = [];
42
43 return {
44 async next() {
45 if (index >= currentPage.length) {
46 if (page >= maxPages) return { done: true };
47 currentPage = await fetchPage(++page);
48 index = 0;
49 }
50 return { value: currentPage[index++], done: false };
51 }
52 };
53}
🔥

pro tip

When building custom iterators, always implement [Symbol.iterator]() { return this; } to make them iterable. This enables use with for...of, spread, and Array.from(). The take helper is essential for working with infinite iterators — always use it to avoid unbounded loops.
Lazy Evaluation

Perhaps the most important benefit of the iterator protocol is lazy evaluation. Values are produced on demand, not upfront. This means you can work with sequences that are infinite, or with large datasets that would not fit in memory if materialized eagerly. The iteration protocol enables this at the language level.

lazy-evaluation.js
JavaScript
1// Lazy vs eager: memory comparison
2
3// Eager — materializes entire result
4function eagerMap(arr, fn) {
5 return arr.map(fn); // Entire new array created
6}
7function eagerFilter(arr, pred) {
8 return arr.filter(pred); // Entire new array created
9}
10
11// Chaining creates multiple intermediate arrays
12const bigData = Array.from({ length: 1000000 }, (_, i) => i);
13const result = eagerFilter(eagerMap(bigData, x => x * 2), x => x > 10);
14// 3 arrays: original, mapped, filtered — millions of items
15
16// Lazy — compose iterators without materializing
17function lazyMap(iterable, fn) {
18 const iter = iterable[Symbol.iterator]();
19 return {
20 next() {
21 const { value, done } = iter.next();
22 return done ? { done } : { value: fn(value), done: false };
23 },
24 [Symbol.iterator]() { return this; }
25 };
26}
27
28function lazyFilter(iterable, pred) {
29 const iter = iterable[Symbol.iterator]();
30 return {
31 next() {
32 let result = iter.next();
33 while (!result.done) {
34 if (pred(result.value)) return { value: result.value, done: false };
35 result = iter.next();
36 }
37 return { done: true };
38 },
39 [Symbol.iterator]() { return this; }
40 };
41}
42
43// No intermediate arrays — values flow through the pipeline
44const pipeline = lazyFilter(lazyMap(bigData, x => x * 2), x => x > 10);
45// Process just the first 5 values
46const first5 = [...take(5, pipeline)];
47console.log(first5); // Only 5 values computed, not 1 million
48
49// This is the same principle as LINQ, Java streams, and functional pipelines

info

Lazy evaluation is not about speed — for small datasets, eager evaluation is often faster. The benefit is memory efficiency and the ability to process infinite or very large sequences. Use lazy pipelines when you have large data volumes, need early termination, or want to compose transformations without materializing intermediate results.
Best Practices
Make custom data structures iterable via [Symbol.iterator] — they automatically work with for...of, spread, destructuring, and Array.from
Use generators (*[Symbol.iterator]()) to implement iterables — they handle the protocol automatically and eliminate boilerplate
Implement return() on iterators that hold resources — it ensures cleanup on early termination (break, error)
Use the take(n) helper pattern for safe consumption of infinite iterators — never spread an infinite iterator directly
Prefer lazy evaluation for large datasets — compose transformations that flow through a pipeline without materializing intermediate arrays
Use Array.from() over spread when you also need a map function — it avoids creating an intermediate array
Understand that objects are NOT iterable by default — use Object.keys(), Object.values(), or Object.entries() to iterate object properties
Use for...of instead of forEach for early termination support — you can break, continue, and return from for...of
Be aware that Map iteration yields [key, value] pairs, not just values — destructure in for...of: for (const [k, v] of map)
Strings iterate by Unicode code points — spread handles multi-byte characters (emojis) correctly, unlike charAt
iterator-quickref.js
JavaScript
1// Quick reference: iteration patterns
2
3// 1. Make any object iterable with generator
4class MyCollection {
5 *[Symbol.iterator]() {
6 yield* this.items;
7 }
8}
9
10// 2. Safe infinite iterator consumption
11function* naturalNumbers() {
12 let n = 1;
13 while (true) yield n++;
14}
15function* take(n, iterable) {
16 let count = 0;
17 for (const val of iterable) {
18 if (count++ >= n) return;
19 yield val;
20 }
21}
22const first100 = [...take(100, naturalNumbers())];
23
24// 3. Lazy pipeline composition
25const pipeline = (iterable) =>
26 take(5, map(x => x * 2, filter(x => x > 10, iterable)));
27
28// 4. Cleanup with return()
29function fileLinesIterator(filePath) {
30 let fileHandle;
31 return {
32 next() { /* read next line */ },
33 return() { fileHandle?.close(); return { done: true }; },
34 [Symbol.iterator]() { return this; }
35 };
36}
37
38// 5. Check if something is iterable
39function isIterable(obj) {
40 return obj != null && typeof obj[Symbol.iterator] === "function";
41}

warning

Not all array-like objects are iterable. The arguments object in non-strict mode and DOM collections like HTMLCollection (returned by getElementsByClassName) are not iterable. Use Array.from() to convert them safely. The NodeList (returned by querySelectorAll) is iterable in modern browsers.
$Blueprint — Engineering Documentation·Section ID: JS-ITER·Revision: 1.0