jsguides

JavaScript Iterator and Iterable Protocols

The JavaScript iterator and iterable protocols define how objects expose their elements one at a time. These two lightweight conventions 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 iteratorMethod = Symbol.iterator;
const obj = {
  [iteratorMethod]: function () {
    // 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), which is 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 the Symbol.iterator method directly:

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

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 a Symbol.iterator method to any object.

Iterable returning a fresh iterator

The most common pattern returns a new iterator object each time the Symbol.iterator method is called. That gives you a fresh iterator for each pass, so you can walk the same source multiple times without the iterators interfering with one another.

const iteratorMethod = Symbol.iterator;
const range = {
  from: 1,
  to: 5,
  [iteratorMethod]: function () {
    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 the Symbol.iterator method. That is the pattern that built-in types like Array use, and it is handy when the collection stores all of its state on the object itself.

const iteratorMethod = Symbol.iterator;
class Counter {
  constructor(limit) {
    this.limit = limit;
    this.current = 0;
    this[iteratorMethod] = function () {
      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 the `Symbol.iterator` method returns a fresh self-iterator each time

The spread operator calls the Symbol.iterator method, 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 and produces 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 asyncIteratorMethod = Symbol.asyncIterator;
const asyncRange = {
  [asyncIteratorMethod]: async function* () {
    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 a Symbol.iterator method and returns an iterator. An iterator has next(). A single object can implement both, like Array, but the roles are still distinct. Confusing them leads to errors such as 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 the Symbol.iterator method again.

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

When to build a custom iterable

Custom iterables are useful when your data has a natural sequence but does not fit a plain array. That can be a range, a stream of parsed records, or a wrapper around a collection that should hide its internal storage. If callers only need to walk through the values once, an iterable gives you a clear contract without exposing more than necessary. The key is to keep the order easy to predict so consumers can reason about the loop without reading the implementation first.

Generators as a Shortcut

Generator functions are often the most direct way to create an iterable in JavaScript. They let you write the steps of the sequence in a natural order while the runtime handles the iterator object for you. That can make the code easier to read than a manual next() implementation, especially when the sequence has a few branching cases. Generators are not mandatory, but they are a good fit when the iteration logic is straightforward and you want the implementation to stay compact.

Consume Carefully

Once you have an iterable, think about who will consume it and how often. Some iterables can be reused, while others are one-shot sequences that should only be read once. Document that behavior if it matters, because callers may otherwise assume the iterable behaves like an array. This is especially important for values backed by external resources. A small note about reuse can prevent confusion when a future caller tries to loop twice and gets nothing the second time.

Debug the Contract

If an iterable is not behaving, start by checking the protocol rather than the consumer. Confirm that the object returns an iterator, that the iterator returns the right shape, and that the end state is reported when expected. Those checks usually uncover the problem faster than stepping through the loop logic itself. The protocol is small, so a small amount of inspection goes a long way. Once the contract is right, the higher-level language features tend to fall back into place.

See Also