Skip to content
JavaScript js control-flow 4 min read

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.

ProtocolRequirementPurpose
IterableA method keyed by Symbol.iterator that returns an iteratorDeclares “I can be looped over”
IteratorAn object with a next() methodProduces 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.
  • donefalse while items remain, true once the sequence is exhausted (at which point value is usually undefined).

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 until done.
  • Spread[...iterable], or spreading into function arguments.
  • Array destructuringconst [first, second] = iterable.
  • Built-ins that accept iterablesArray.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 }; omitting done confuses consumers.
  • Prefer generator functions over hand-written next() logic — they are shorter and harder to get wrong.
  • Keep Symbol.iterator returning 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.
Last updated June 1, 2026
Was this helpful?