JavaScript Iterable Protocol: Complete Guide

The iterable protocol is the contract that lets any object work with for...of, spread, and destructuring. Learn how Symbol.iterator makes your data structures work with JavaScript's iteration syntax.

6 min read

The iterable protocol is a contract in JavaScript: if an object has a method keyed by Symbol.iterator, and that method returns an iterator, then the object is iterable. Once an object is iterable, it works with for...of, the spread operator, Array.from, and destructuring.

That is the entire protocol. One method, one return type, and your data structure plugs into every iteration feature in the language.

javascriptjavascript
// The contract in its simplest form
const iterable = {
  [Symbol.iterator]() {
    return {
      next() {
        return { value: "anything", done: false };
      }
    };
  }
};

If the iterator in the example above never returned done set to true, for...of would loop forever. Real iterables stop eventually.

The Iterable Protocol vs the Iterator Protocol

These two protocols are separate but designed to work together:

Iterable and iterator protocol relationship
Iterable protocolIterator protocol
RequiresA Symbol.iterator methodA next() method
Method takesZero argumentsZero arguments
Method returnsAn iteratorValue and done properties

A single object can satisfy both protocols by having its Symbol.iterator method return this and defining next(). Generators do this automatically. For how Symbol.iterator works as a property key, see /javascript/javascript-symbol-type-complete-guide.

How for...of Consumes Iterables

When you write a for...of loop, the engine does this:

  • Calls the iterable's Symbol.iterator method to get a fresh iterator.
  • Calls next() on that iterator to get the first value.
  • If done is false, binds the value to the loop variable and runs the body.
  • Repeats the last two steps until done is true.
javascriptjavascript
const greeting = "Hi";
 
// The engine does this behind the scenes:
const it = greeting[Symbol.iterator]();
 
console.log(it.next()); // { value: "H", done: false }
console.log(it.next()); // { value: "i", done: false }
console.log(it.next()); // { value: undefined, done: true }

Strings are iterable because String.prototype defines a Symbol.iterator method. Every character becomes an item. This is why spreading a string into an array splits it into individual characters.

Built-In Iterables

JavaScript ships with several iterable types. Each has a Symbol.iterator method on its prototype:

IterableIterates overExample iteration
ArrayEach elementfor (const el of [1,2,3])
StringEach Unicode code pointfor (const ch of "abc")
Mapkey/value pairsfor (const [k,v] of map)
SetEach value in insertion orderfor (const v of set)
TypedArrayEach numeric elementfor (const n of new Uint8Array([1,2]))
NodeListEach DOM nodefor (const el of document.querySelectorAll("div"))
argumentsEach function argumentfor (const arg of arguments)

Plain objects are not iterable. You must use Object.keys(), Object.values(), or Object.entries() instead:

javascriptjavascript
const obj = { a: 1, b: 2 };
 
// Not iterable -- this throws TypeError
// for (const item of obj) {}
 
// Use Object methods instead
for (const [key, value] of Object.entries(obj)) {
  console.log(`${key}: ${value}`);
}

Building a Custom Iterable

Any class can become iterable by adding a [Symbol.iterator] method to it. Here is a linked list that you can iterate with a for...of loop once that method is in place:

javascriptjavascript
class LinkedList {
  constructor() {
    this.head = null;
  }
  add(value) {
    const node = { value, next: this.head };
    this.head = node;
  }
  [Symbol.iterator]() {
    let current = this.head;
    return {
      next() {
        if (current) {
          const value = current.value;
          current = current.next;
          return { value, done: false };
        }
        return { value: undefined, done: true };
      }
    };
  }
}

The [Symbol.iterator] method builds a fresh next closure every time it runs, starting from the head node. Add a few nodes and the list works with both for...of and spread:

javascriptjavascript
const list = new LinkedList();
list.add("c");
list.add("b");
list.add("a");
 
// Works with for...of
for (const item of list) {
  console.log(item); // a, b, c
}
 
// Works with spread
console.log([...list]); // ["a", "b", "c"]

The [Symbol.iterator] method creates a closure around a local current variable, which tracks the iteration position. Each call returns a fresh iterator with its own pointer, so you can iterate the same list multiple times.

An Object That Is Both Iterable and Iterator

When an object's Symbol.iterator method returns itself, the object acts as both the iterable and its own iterator:

javascriptjavascript
const range = {
  from: 1,
  to: 5,
 
  [Symbol.iterator]() {
    this.current = this.from;
    return this; // Returning itself
  },
 
  next() {
    if (this.current > this.to) {
      return { done: true };
    }
    return { value: this.current++, done: false };
  }
};
 
console.log([...range]); // [1, 2, 3, 4, 5]
 
// A second spread still works, because Symbol.iterator resets current every time it runs
console.log([...range]); // [1, 2, 3, 4, 5]

This pattern is concise but has a tradeoff: because the iterator state lives directly on range, two iterations running at the same time, such as a nested for...of loop over the same object, would share and corrupt each other's position. Sequential, fully-finished iterations are safe. Overlapping ones are not.

For most custom iterables, return a fresh iterator object from Symbol.iterator instead of this. The linked list example above follows this safer pattern.

The Optional return() and throw() Methods

An iterator returned by Symbol.iterator can optionally define return() and throw():

javascriptjavascript
const closeable = {
  data: [1, 2, 3, 4, 5],
 
  [Symbol.iterator]() {
    let index = 0;
    const data = this.data;
 
    return {
      next() {
        if (index >= data.length) return { done: true };
        return { value: data[index++], done: false };
      },
      return() {
        console.log("Cleanup: iterator closed early");
        index = data.length; // Mark as exhausted
        return { done: true };
      }
    };
  }
};

Breaking out of the loop below before it finishes still triggers return(), giving the iterator a chance to clean up:

javascriptjavascript
for (const n of closeable) {
  console.log(n);
  if (n === 2) break;
}
// 1
// 2
// Cleanup: iterator closed early

The for...of loop calls return() when it exits early via a break statement, a return statement, or an error. Use it to release resources like file handles or event listeners.

throw() is called by for...of when an error occurs inside the loop body. It is rarely needed in custom iterators, but generators use it to inject errors at yield points.

Iterables and Destructuring

Destructuring works with any iterable, not just arrays:

javascriptjavascript
const [first, second] = new Set(["x", "y", "z"]);
console.log(first, second); // x y
 
const [head, ...tail] = "hello";
console.log(head); // "h"
console.log(tail); // ["e", "l", "l", "o"]
 
const map = new Map([["a", 1], ["b", 2]]);
const [[key, value], ...rest] = map;
console.log(key, value); // a 1

Any iterable can appear on the right side of a destructuring assignment. The engine calls its Symbol.iterator method and pulls values one at a time.

Iterable Consumers Summary

These language features all consume iterables:

FeatureWhat it does with the iterable
for...ofIterates and runs the loop body for each value
Spread (...)Spreads values into a new array
Array.from(iterable)Creates an array from the iterable
new Map(iterable)Populates a Map from key/value pairs
new Set(iterable)Populates a Set from values
Promise.all(iterable)Waits for all promises in the iterable
Promise.race(iterable)Races promises from the iterable
DestructuringPulls values from the start of the iterable
for await...ofAsync version, uses Symbol.asyncIterator

All of these are built on the same protocol. Make an object iterable once, and it works with every consumer.

For the underlying iterator mechanics, see /javascript/javascript-iterators-complete-guide. For generators, which are the easiest way to build iterables, see /javascript/javascript-generators-deep-dive-full-guide.

Rune AI

Rune AI

Key Insights

  • An iterable is any object with a Symbol.iterator method that returns an iterator.
  • The iterator must have a next() method returning { value, done } objects.
  • for...of, spread, Array.from, and destructuring all consume iterables via this protocol.
  • Built-in iterables include Array, String, Map, Set, TypedArray, and NodeList.
  • Objects are not iterable by default, but you can make any object iterable by defining Symbol.iterator.
RunePowered by Rune AI

Frequently Asked Questions

Why aren't plain objects iterable by default?

Objects do not have a guaranteed iteration order for their keys, and their keys can be strings or symbols. The language designers left iteration to explicit methods (Object.keys, Object.values, Object.entries) so you choose the iteration behavior you want.

Can I make a class both iterable and an iterator?

Yes. Implement Symbol.iterator to return this, and define a next() method. This pattern is used by generators and works well for stateful iterators that are consumed once. But if you need multiple independent iterations, return a fresh iterator from Symbol.iterator instead.

Conclusion

The iterable protocol is a single-method contract: implement Symbol.iterator, return an object with next(), and your data structure works with for...of, spread, Array.from, and destructuring. It is one of the most impactful design decisions in modern JavaScript because it unifies iteration across arrays, strings, Maps, Sets, DOM collections, and custom types under one simple interface.