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.
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.
// 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 protocol | Iterator protocol | |
|---|---|---|
| Requires | A Symbol.iterator method | A next() method |
| Method takes | Zero arguments | Zero arguments |
| Method returns | An iterator | Value 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.
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:
| Iterable | Iterates over | Example iteration |
|---|---|---|
| Array | Each element | for (const el of [1,2,3]) |
| String | Each Unicode code point | for (const ch of "abc") |
| Map | key/value pairs | for (const [k,v] of map) |
| Set | Each value in insertion order | for (const v of set) |
| TypedArray | Each numeric element | for (const n of new Uint8Array([1,2])) |
| NodeList | Each DOM node | for (const el of document.querySelectorAll("div")) |
| arguments | Each function argument | for (const arg of arguments) |
Plain objects are not iterable. You must use Object.keys(), Object.values(), or Object.entries() instead:
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:
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:
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:
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():
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:
for (const n of closeable) {
console.log(n);
if (n === 2) break;
}
// 1
// 2
// Cleanup: iterator closed earlyThe 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:
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 1Any 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:
| Feature | What it does with the iterable |
|---|---|
| for...of | Iterates 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 |
| Destructuring | Pulls values from the start of the iterable |
| for await...of | Async 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
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.
Frequently Asked Questions
Why aren't plain objects iterable by default?
Can I make a class both iterable and an iterator?
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.
More in this topic
Using Decorators for Logging in JS Architecture
Learn how JavaScript decorators wrap class methods to add logging without touching the original code, including setup with Babel and how execution order works.
JavaScript While Loop Explained: A Complete Guide
A while loop repeats code as long as a condition stays true. Learn the syntax, see practical examples, and understand when a while loop is the right choice over a for loop.
JavaScript Loops Tutorial: for, while, do while
Loops let JavaScript repeat code without writing it twice. Learn the three core loop types, what each one does, and when to choose one over another.