JavaScriptIterators

Iterators

An iterator is a small object that knows how to produce values one at a time. JavaScript's iteration features — for...of, spread, destructuring, Array.from — all sit on top of one tiny contract called the iterator protocol. Understanding it unlocks generators, custom iterables, and a much clearer mental model of how loops actually work.

The iterator protocol

An iterator is any object with a next() method that returns { value, done }:

  • value — the next item.

  • donefalse while more values exist, true when iteration is finished. When done is true, value is usually undefined.

Driving an array iterator by hand

JS
const arr = ["a", "b", "c"];
const it = arr[Symbol.iterator]();

console.log(it.next());   // { value: 'a', done: false }
console.log(it.next());   // { value: 'b', done: false }
console.log(it.next());   // { value: 'c', done: false }
console.log(it.next());   // { value: undefined, done: true }
{ value: 'a', done: false }
{ value: 'b', done: false }
{ value: 'c', done: false }
{ value: undefined, done: true }
The iterable protocol

An iterable is any object that has a method at the well-known symbol Symbol.iterator which returns a fresh iterator. for...of and spread look for that method first — anything that has it can be iterated.

JS
function isIterable(x) {
  return x != null && typeof x[Symbol.iterator] === "function";
}

isIterable([1, 2]);          // true
isIterable("hello");         // true
isIterable(new Set());       // true
isIterable({ a: 1 });        // false

Plain objects are not iterable by default. Arrays, strings, Map, Set, NodeList, typed arrays, generators and arguments all are.

Consuming with for...of

for...of does the protocol work for you: it calls Symbol.iterator, calls next() repeatedly, and stops when done is true.

What for...of is doing under the hood

JS
function forEach(iterable, fn) {
  const it = iterable[Symbol.iterator]();
  while (true) {
    const { value, done } = it.next();
    if (done) break;
    fn(value);
  }
}

forEach(["a", "b", "c"], console.log);
a
b
c
Building a custom iterable

Make any object iterable by adding a [Symbol.iterator] method. It must return an iterator — an object with next().

A range that knows how to iterate itself

JS
function range(start, end, step = 1) {
  return {
    [Symbol.iterator]() {
      let current = start;
      return {
        next() {
          if (current < end) {
            const value = current;
            current += step;
            return { value, done: false };
          }
          return { value: undefined, done: true };
        },
      };
    },
  };
}

for (const n of range(0, 5)) console.log(n);
console.log([...range(0, 5)]);
console.log([...range(0, 10, 2)]);
0
1
2
3
4
[ 0, 1, 2, 3, 4 ]
[ 0, 2, 4, 6, 8 ]
Iterables that produce themselves

An iterator can also be an iterable — return this from its Symbol.iterator method. That's how built-in iterators like array.entries() work: they can be used directly with for...of.

JS
function counter(max) {
  let i = 0;
  return {
    next() {
      return i < max ? { value: i++, done: false } : { value: undefined, done: true };
    },
    [Symbol.iterator]() { return this; },
  };
}

for (const n of counter(3)) console.log(n);
0
1
2
Where iterators show up

Once an object is iterable, all of these features just work:

  • for...of loops.

  • Array/spread destructuring: const [a, b] = iterable.

  • Spread into arrays or arguments: [...iterable], Math.max(...iterable).

  • Array.from(iterable) and Array.from(iterable, fn).

  • new Map(iterable), new Set(iterable) — when the items are pairs or values.

  • Promise.all(iterable) and friends.

One protocol, dozens of features
This is the beauty of the iterator protocol — implement `Symbol.iterator` once and a whole family of language features suddenly accept your object.
Early termination — return()

Iterators may also implement an optional return() method. It is called by for...of when the loop exits early (break, throw, return), giving the iterator a chance to clean up — close a file handle, release a resource, finish a transaction.

JS
function cleanupRange(n) {
  let i = 0;
  return {
    next() {
      return i < n ? { value: i++, done: false } : { value: undefined, done: true };
    },
    return(value) {
      console.log("cleanup at", i);
      return { value, done: true };
    },
    [Symbol.iterator]() { return this; },
  };
}

for (const n of cleanupRange(10)) {
  if (n === 3) break;
}
cleanup at 4
Lazy by nature

Iterators only produce values when asked. That makes them ideal for infinite or expensive sequences — you don't materialise the whole thing into an array.

JS
const naturals = {
  [Symbol.iterator]() {
    let i = 1;
    return { next() { return { value: i++, done: false }; } };
  },
};

// Pull the first five — the iterator stops when we stop asking.
const it = naturals[Symbol.iterator]();
for (let i = 0; i < 5; i++) console.log(it.next().value);
1
2
3
4
5

Writing iterators by hand is verbose. The next page, Generators, shows how function* and yield let you write iterators that look like ordinary code.

One sentence to remember
An iterator is just an object with `next()` returning `{ value, done }` — implement `[Symbol.iterator]` and your object plugs into every iteration feature the language has.