Iterables & Iteration Protocols
Many JavaScript features — for...of, the spread syntax, array destructuring — appear to “just work” on arrays, strings, Maps, and Sets. Behind that convenience are two small contracts called the iteration protocols, standardized in ES2015. Once you understand them, you can make your own objects participate in the same language machinery, and you’ll know exactly why some values are iterable and others are not.
Two protocols, one system
The spec defines two cooperating protocols. They sound similar but play different roles.
| Protocol | Requirement | Purpose |
|---|---|---|
| Iterable | A method keyed by Symbol.iterator that returns an iterator | Declares “I can be looped over” |
| Iterator | An object with a next() method | Produces the values, one at a time |
An object is iterable if it has a method under the well-known symbol Symbol.iterator. Calling that method must return an iterator: an object whose next() method returns a result object shaped like { value, done }.
value— the next item in the sequence.done—falsewhile items remain,trueonce the sequence is exhausted (at which pointvalueis usuallyundefined).
for...of, spread, and destructuring all call Symbol.iterator first, then repeatedly call next() until done is true. That is the entire mechanism.
Inspecting a built-in iterator
Arrays are iterable, so they expose Symbol.iterator. You can drive the iterator by hand to see the protocol in action:
const letters = ["a", "b"];
const it = letters[Symbol.iterator]();
console.log(it.next());
console.log(it.next());
console.log(it.next());
Output:
{ value: 'a', done: false }
{ value: 'b', done: false }
{ value: undefined, done: true }
A for...of loop does precisely this internally, stopping the moment done becomes true.
Iterators are usually single-use: once exhausted they keep returning
{ value: undefined, done: true }. To loop again, request a fresh iterator from the iterable.
Building a custom iterable
To make your own object iterable, add a [Symbol.iterator]() method that returns an iterator. Here is a range object that yields numbers from start up to (but not including) end:
const range = {
start: 1,
end: 5,
[Symbol.iterator]() {
let current = this.start;
const last = this.end;
return {
next() {
if (current < last) {
return { value: current++, done: false };
}
return { value: undefined, done: true };
},
};
},
};
for (const n of range) {
console.log(n);
}
console.log([...range]);
console.log(Math.max(...range));
Output:
1
2
3
4
[ 1, 2, 3, 4 ]
4
Because range satisfies the iterable protocol, every iterable-aware feature works on it for free — for...of, spread into an array, and spread into a function call all behave identically.
What this protocol powers
Implementing one method unlocks a surprising amount of syntax:
for...of— walks the values untildone.- Spread —
[...iterable], or spreading into function arguments. - Array destructuring —
const [first, second] = iterable. - Built-ins that accept iterables —
Array.from(),new Map(),new Set(),Promise.all(), and more.
This is also why a plain object ({}) is not iterable: it has no Symbol.iterator, so for...of on it throws TypeError: obj is not iterable. Use Object.entries() to get an iterable view instead.
Generators: iterators made easy
Writing next() by hand is verbose and easy to get wrong. Generator functions (function*) produce iterator objects automatically — each yield becomes a { value, done } step, and the function pauses between calls. A generator’s return value is both an iterator and iterable, so the same range becomes:
function* range(start, end) {
for (let n = start; n < end; n++) {
yield n;
}
}
console.log([...range(1, 5)]);
Output:
[ 1, 2, 3, 4 ]
For most custom sequences, reach for a generator first and only hand-roll an iterator when you need precise control over the result objects.
Best practices
- Make an object iterable by adding a
[Symbol.iterator]()method — never fake it with array-like indexes. - Always return result objects shaped as
{ value, done }; omittingdoneconfuses consumers. - Prefer generator functions over hand-written
next()logic — they are shorter and harder to get wrong. - Keep
Symbol.iteratorreturning a fresh iterator each call so the iterable can be looped multiple times. - Remember that strings,
Map,Set,NodeList, and arguments are already iterable — no need to convert them first. - Guard against infinite iterables (generators with no termination) when spreading, since spread consumes the whole sequence.