Iterators and the iterator protocol — what makes for...of actually work
for...of, spread syntax, and destructuring all work on arrays, strings, Maps, and Sets — but not on plain objects — because those types all implement one specific, real protocol under the hood, and understanding that protocol is what makes a custom object work with all three too.
3 min read
The question this lesson answers: why does for...of work on some things, but not others
for (const char of "hello") { console.log(char); } // works — strings are iterable
for (const n of [1, 2, 3]) { console.log(n); } // works — arrays are iterable
for (const key of { a: 1, b: 2 }) { console.log(key); } // TypeError — plain objects are NOT iterablefor...of (and spread syntax, and array/object destructuring of the form const [a, b] = x) doesn't work on every value — it works specifically on values that implement the iterable protocol, a real, defined interface, not an arbitrary list of "types that happen to support this." Plain objects don't implement it by default, which is exactly why for...of on a plain object throws, while the same syntax works fine on an array, a string, a Map, or a Set.
The actual mechanism: an object with a Symbol.iterator method
const range = {
from: 1,
to: 3,
[Symbol.iterator]() { // this ONE method is what makes something iterable
let current = this.from;
const last = this.to;
return {
next() { // the ITERATOR itself — called repeatedly to get each value
if (current <= last) {
return { value: current++, done: false };
}
return { value: undefined, done: true };
},
};
},
};
for (const n of range) { console.log(n); } // 1, 2, 3 — works, because Symbol.iterator was implementedSymbol.iterator (a real, built-in "well-known symbol" — a special, unique key JavaScript itself defines specifically for this purpose) is the one method the iterable protocol actually requires: an object with a [Symbol.iterator]() method returning an iterator — an object with its own next() method, which returns { value, done } each time it's called, is genuinely iterable. for...of calls [Symbol.iterator]() once to get the iterator, then calls .next() repeatedly until done is true — this is the entire mechanism, for every iterable type in the language, arrays and strings included.
Why arrays, strings, Map, and Set all "just work" with for...of, spread, and destructuring
[...range]; // [1, 2, 3] — spread ALSO uses Symbol.iterator
const [first] = range; // 1 — destructuring ALSO uses Symbol.iterator
Array.from(range); // [1, 2, 3] — Array.from ALSO uses Symbol.iteratorEvery one of these language features — for...of, spread syntax, array destructuring, Array.from — is built on the exact same Symbol.iterator protocol, not four separate, unrelated mechanisms that each happen to support arrays. This is why implementing Symbol.iterator on the custom range object above makes it work with all of them simultaneously, for free — they all go through the identical underlying mechanism, just with different syntax on the calling side.
Why plain objects deliberately don't implement this by default
const obj = { a: 1, b: 2, c: 3 };
Object.keys(obj); // the INTENDED way to iterate a plain object's keys
Object.entries(obj); // or key-value pairs togetherPlain objects were deliberately never given a default Symbol.iterator implementation, specifically because "iterate this object" is ambiguous in a way arrays aren't — should it yield keys, values, or entries? Rather than guessing, JavaScript requires an explicit choice via Object.keys(), Object.values(), or Object.entries() (each of which does return a genuinely iterable array), leaving for...of reserved for types where the intended iteration order and content are unambiguous by design.
Further reading
Check your understanding
A quick comprehension check — not tracked, not graded, just for you.
1. Why does `for...of` work on arrays and strings but throw a TypeError on a plain object?
2. What does an object need to implement to be genuinely iterable?
3. Why don't plain objects get a default Symbol.iterator implementation, unlike arrays?