Skip to content
JavaScript js collections 4 min read

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:

PropertyTypeMeaning
valueanyThe current item produced by this step
donebooleanfalse 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 with next()). Reusing a single counter across loops causes the second for...of to 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.iterator each time so the iterable can be looped over more than once.
  • Always include done: true on the terminating result; an iterator that never reports done creates an infinite loop.
  • Prefer generators (function*) over manual next() 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...in iterates object keys, while for...of uses the iterator protocol; they are not interchangeable.
Last updated June 1, 2026
Was this helpful?