The Iterator and Iterable Protocols
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;undefinedwhen iteration is finished)done— a boolean:falsewhile items remain,trueafter 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:
| Type | Yields |
|---|---|
Array | Elements |
String | Characters (Unicode grapheme clusters) |
TypedArray | Elements |
Map | [key, value] pairs |
Set | Elements |
NodeList | DOM 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
| Feature | Added |
|---|---|
Symbol.iterator, for...of, spread, Array.from() | ES6 (2015) |
| NodeList / DOMTokenList iteration | ES2016 |
Symbol.asyncIterator, for await...of | ES2018 |
Array.fromAsync() | ES2024 |
See Also
- JavaScript Async Iterators — consuming and creating async iterables
- JavaScript Generators — a simpler syntax for building iterators
- JavaScript Symbols —
Symbol.iteratorand other well-known symbols