The Iterator and Iterable Protocols

· 6 min read · Updated April 1, 2026 · intermediate
javascript iteration symbols es6

JavaScript’s iteration protocols define how objects expose their elements one at a time. These two lightweight conventions — the iterable protocol and the iterator protocol — are the foundation for for...of, the spread operator, Array.from(), and dozens of other built-in APIs. Once you understand how they work, you can make any object fit seamlessly into JavaScript’s iteration machinery.

The Iterable Protocol

An object is iterable when it implements a method keyed by Symbol.iterator. That method takes no arguments and returns an iterator — a separate object with a next() method.

const obj = {
  [Symbol.iterator]() {
    // returns an iterator object
    return {
      next() {
        return { value: 1, done: false };
      }
    };
  }
};

Any object with this method is iterable and can be used with for...of, spread syntax (...), Array.from(), the Map() and Set() constructors, and more.

The Iterator Protocol

The iterator protocol defines what next() must return. Every call to next() produces an object with two properties:

  • value — the current item (any JavaScript value; undefined when iteration is finished)
  • done — a boolean: false while items remain, true after the last item has been yielded
const counter = {
  count: 1,
  next() {
    if (this.count > 3) {
      return { value: undefined, done: true };
    }
    return { value: this.count++, done: false };
  }
};

console.log(counter.next()); // { value: 1, done: false }
console.log(counter.next()); // { value: 2, done: false }
console.log(counter.next()); // { value: 3, done: false }
console.log(counter.next()); // { value: undefined, done: true }

Iterators may also implement optional return() and throw() methods. return() fires when iteration ends early (via break, return, or an error) — useful for releasing resources. throw() is primarily for generator internals and rarely used in hand-written iterators. Both are optional; most custom iterators only need next().

Built-in Iterables

Most built-in JavaScript types are iterable out of the box:

TypeYields
ArrayElements
StringCharacters (Unicode grapheme clusters)
TypedArrayElements
Map[key, value] pairs
SetElements
NodeListDOM nodes

You can see this in action by calling [Symbol.iterator]() directly:

const arr = [10, 20, 30];
const iter = arr[Symbol.iterator]();

console.log(iter.next().value); // 10
console.log(iter.next().value); // 20
console.log(iter.next().value); // 30
console.log(iter.next().done);  // true

Custom Iterables and Iterators

You can define your own iteration behaviour by adding [Symbol.iterator]() to any object.

Iterable returning a fresh iterator

The most common pattern returns a new iterator object each time [Symbol.iterator]() is called. This lets you iterate over the same source multiple times without the iterators interfering with each other.

const range = {
  from: 1,
  to: 5,
  [Symbol.iterator]() {
    let current = this.from;
    return {
      next() {
        if (current > this.to) {
          return { value: undefined, done: true };
        }
        return { value: current++, done: false };
      }
    };
  }
};

for (const num of range) {
  console.log(num); // 1, 2, 3, 4, 5
}

const [first, second] = range;
console.log(first, second); // 1, 2

Note that inside next(), this refers to the iterator object itself, not the original range object. The closure captures current and this.to at creation time, which is why this pattern works correctly.

Iterator class

You can also make a single object act as both iterable and iterator by returning this from [Symbol.iterator](). This is the pattern that built-in types like Array use.

class Counter {
  constructor(limit) {
    this.limit = limit;
    this.current = 0;
  }

  [Symbol.iterator]() {
    return this;
  }

  next() {
    if (this.current >= this.limit) {
      return { value: undefined, done: true };
    }
    return { value: ++this.current, done: false };
  }
}

const counter = new Counter(3);
console.log([...counter]); // [1, 2, 3]
console.log([...counter]); // [1, 2, 3] — calling [Symbol.iterator]() returns a fresh self-iterator each time

The spread operator calls [Symbol.iterator](), which returns this (the Counter instance), then calls next() until done is true. Because Counter stores its state in instance properties (current), each new Counter(3) instance starts fresh, so you get a new iteration.

Using Iterables with Spread and Destructuring

Any iterable can be consumed with spread syntax or destructuring:

const doubled = [...range, ...[100, 200]];
console.log(doubled); // [1, 2, 3, 4, 5, 100, 200]

const [head, ...tail] = range;
console.log(head); // 1
console.log(tail); // [2, 3, 4, 5]

This works with strings, arrays, Maps, Sets, and any custom iterable you write.

Async Iterators

ES2018 introduced async iterators for iteration over asynchronous data sources. The protocol uses Symbol.asyncIterator instead of Symbol.iterator, and next() must return a Promise that resolves to the { value, done } object.

const asyncRange = {
  async [Symbol.asyncIterator]() {
    for (let i = 1; i <= 3; i++) {
      await new Promise(resolve => setTimeout(resolve, 50));
      yield { value: i, done: false };
    }
    return { value: undefined, done: true };
  }
};

async function demo() {
  for await (const num of asyncRange) {
    console.log(num); // 1, 2, 3
  }
}

demo();

You consume async iterables with for await...of. Note that next() must return a Promise. The for await...of loop awaits each Promise and extracts { value, done } from the resolved value — so each iteration pauses while waiting for the async operation to complete.

ES2024 added Array.fromAsync(), which accepts both sync and async iterables and returns a Promise that resolves to an array of the collected values.

Common Pitfalls

Iterables and iterators are not the same thing. An iterable has [Symbol.iterator]() and returns an iterator. An iterator has next(). A single object can implement both (like Array), but the roles are distinct. Confusing them leads to errors like trying to call next() directly on a plain array — arrays are iterable, not iterators, so arr.next() is undefined.

Iterators are single-use. Once next() returns { done: true }, the iterator is exhausted. Calling next() again keeps returning { value: undefined, done: true }. If you need to iterate again, create a new iterator by calling [Symbol.iterator]().

Plain objects are not iterable. { a: 1 }[Symbol.iterator] is undefined. You cannot use for...of or spread on a plain object without adding a custom [Symbol.iterator] implementation.

Strings yield characters, not code units. For emoji and other multi-codepoint grapheme clusters, for...of counts visible characters, not UTF-16 code units. "👍".length === 2 but [..."👍"].length === 1.

Infinite iterators loop forever. An iterator with no terminating condition will yield { done: false } indefinitely. for...of on an infinite iterator never exits, and Array.from() on one will crash with a memory error.

Language History

FeatureAdded
Symbol.iterator, for...of, spread, Array.from()ES6 (2015)
NodeList / DOMTokenList iterationES2016
Symbol.asyncIterator, for await...ofES2018
Array.fromAsync()ES2024

See Also