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.

Intermediate

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 iterable

for...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 implemented

Symbol.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.iterator

Every 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 together

Plain 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?