Iterators
An iterator is the engine behind every for...of loop, spread, and destructuring in JavaScript. Standardized in ES2015, the iterator protocol is a tiny contract that any object can implement to declare “you can step through me, one value at a time.” Once an object follows that contract, all the language’s iteration machinery — for...of, the spread operator, Array.from, destructuring — works on it for free. Understanding the protocol turns iteration from magic into a tool you can build yourself.
The iterator protocol
An object is an iterator when it has a next() method that returns an object with two properties:
| Property | Type | Meaning |
|---|---|---|
value | any | The current item produced by this step |
done | boolean | false while items remain, true once exhausted |
Each call to next() advances the sequence and hands back the next result. When done becomes true, iteration stops. Here is a hand-built iterator with no helper at all:
function makeCounter(limit) {
let count = 0;
return {
next() {
if (count < limit) {
return { value: count++, done: false };
}
return { value: undefined, done: true };
},
};
}
const it = makeCounter(3);
console.log(it.next()); // { value: 0, done: false }
console.log(it.next()); // { value: 1, done: false }
console.log(it.next()); // { value: 2, done: false }
console.log(it.next()); // { value: undefined, done: true }
Output:
{ value: 0, done: false }
{ value: 1, done: false }
{ value: 2, done: false }
{ value: undefined, done: true }
This object is an iterator, but it is not yet iterable — you cannot drop it into a for...of loop. That requires one more piece.
Making an object iterable with Symbol.iterator
An object is iterable when it has a method keyed by the well-known symbol Symbol.iterator that returns an iterator. for...of, spread, and friends call this method to obtain a fresh iterator, then repeatedly call next() on it. Built-ins like arrays, strings, Map, and Set all ship with a Symbol.iterator method already.
The classic example is a numeric range that you can loop over without ever materializing an array:
const range = {
start: 1,
end: 5,
[Symbol.iterator]() {
let current = this.start;
const last = this.end;
return {
next() {
return current <= last
? { value: current++, done: false }
: { value: undefined, done: true };
},
};
},
};
for (const n of range) {
console.log(n);
}
Output:
1
2
3
4
5
A subtle but important detail: Symbol.iterator returns a new iterator object on every call, with its own current counter. That is what lets you loop over the same range twice and start fresh each time.
Tip: Keep the iterable (the object with
Symbol.iterator) separate from the iterator (the object withnext()). Reusing a single counter across loops causes the secondfor...ofto find an already-exhausted iterator and produce nothing.
How for…of and spread consume an iterable
Every iteration-aware feature speaks the same protocol under the hood. Once range is iterable, all of these just work:
console.log([...range]); // spread into an array
console.log(Array.from(range)); // explicit conversion
const [first, second] = range; // array destructuring
console.log(first, second);
// Spread also flows into function calls
console.log(Math.max(...range));
Output:
[ 1, 2, 3, 4, 5 ]
[ 1, 2, 3, 4, 5 ]
1 2
5
Behind the scenes, for...of is roughly equivalent to manually calling Symbol.iterator and looping on next():
const iterator = range[Symbol.iterator]();
let result = iterator.next();
while (!result.done) {
console.log(result.value);
result = iterator.next();
}
This is exactly why implementing the protocol once unlocks the entire family of consuming syntaxes at no extra cost.
Bridging to generators
Writing iterators by hand is verbose: you juggle state in a closure and carefully shape { value, done } objects. Generators are syntactic sugar that produces protocol-compliant iterators automatically. A generator function (function*) returns an object that is both an iterator and iterable, and each yield becomes a next() step.
The same range, rewritten as a generator, collapses to a few lines:
function* range(start, end) {
for (let n = start; n <= end; n++) {
yield n;
}
}
console.log([...range(1, 5)]); // [ 1, 2, 3, 4, 5 ]
Output:
[ 1, 2, 3, 4, 5 ]
You can even use a generator method as the Symbol.iterator of an object to make it iterable with almost no boilerplate. For most real code, reach for generators first and only hand-roll an iterator when you need precise control over the returned object.
Best Practices
- Return a fresh iterator from
Symbol.iteratoreach time so the iterable can be looped over more than once. - Always include
done: trueon the terminating result; an iterator that never reportsdonecreates an infinite loop. - Prefer generators (
function*) over manualnext()implementations — they are shorter and harder to get wrong. - Keep the iterator’s state local (in a closure or the generator frame), not on the shared iterable object.
- Use
for...of, spread, and destructuring to consume iterables rather than indexing — they work on any iterable, not just arrays. - Remember that
for...initerates object keys, whilefor...ofuses the iterator protocol; they are not interchangeable.