The iteration protocols
Learn the Symbol.iterator and next() protocol behind for...of, spread, Array.from, destructuring, Map, Set, strings, and your own custom iterable objects.
- 01Explain the protocolDescribe how
[Symbol.iterator]()returns an iterator withnext()results. - 02Predict consumersTrace how
for...of, spread, destructuring, andMapconsume iterables. - 03Build and debug iterablesCreate a custom range and fix
TypeError: object is not iterable.
The hidden agreement behind loops
In the for…of & for…in lesson, you used for...of to ask arrays and strings for their values. This lesson opens the hood. An iterable is any value that can produce an iterator. An iterator is the object that hands values out one at a time.
That small agreement is why one set of tools works with many kinds of data: for...of, spread, Array.from, destructuring, Promise.all, and new Map all know how to consume iterable values.
Ask the machine for a dispenser, then press the dispenser for one item at a time. The machine can hand another customer a fresh dispenser. One dispenser remembers where it is.
- In real life: A vending machine
- In JavaScript: An iterable object
- In real life: The button that hands you a dispenser
- In JavaScript: The
[Symbol.iterator]()method - In real life: The dispenser
- In JavaScript: The iterator object
- In real life: Pressing once for one snack
- In JavaScript: Calling
next()for one{ value, done }result
Where the analogy stops: A real vending machine stores a fixed shelf of snacks. A JavaScript iterable may calculate values lazily, read from another data source, or even represent a sequence that is too large to store.
| Term | Required method | What it returns | Typical example |
|---|---|---|---|
| Iterable | [Symbol.iterator]() | An iterator object | Array, string, Map, Set, custom range |
| Iterator | next() | { value, done } result objects | The object returned by an array iterator |
| Iterable iterator | Both methods | Itself from [Symbol.iterator]() | Built-in array, string, Map, and Set iterators |
The precision matters because the words sound almost the same. An array is iterable, but it is not itself an iterator. The object returned by array[Symbol.iterator]() is the iterator.
Symbol.iterator: the method consumers look for
FOUNDATIONThe iterable protocol is a method keyed by the built-in symbol Symbol.iterator. Symbols, from the Symbols lesson, make excellent protocol keys because they do not collide with ordinary property names like iterator.
const iterable = { [Symbol.iterator]() { return iterator; },}; const iterator = { next() { return { done: true }; },};Real iterables usually create the iterator inside the method, so each call starts fresh. Below, range(1, 5, step) creates a custom iterable. The iterable is reusable; the iterator it creates is one trip through the numbers.
JavaScript does not require a special class. If an object has a callable [Symbol.iterator] method returning an object with next(), consumers can use it.
next() and done: the result objects
STEP THROUGHAn iterator’s job is tiny and strict: every next() call returns an object. When there is a value, the result is { value, done: false }. When the sequence is over, the result has done: true. A consumer stops there.
Predict each result object before stepping. Change the step to make the same iterable count differently.
script
const iterator = numbers[Symbol.iterator](); console.log(iterator.next());console.log(iterator.next());console.log(iterator.next());console.log(iterator.next()); function range(start, end, step = 1) { return { [Symbol.iterator]() { let current = start; return { next() { if (current > end) return { done: true }; const value = current; current = current + step; return { value, done: false }; }, }; }, };}Notice the final result. for...of looks at done; if it is true, the loop body does not run. Even if a custom iterator includes a value on that final result, a for...of loop ignores it.
const source = "A😀B";const iterator = source[Symbol.iterator](); iterator.next(); // { value: "A", done: false }iterator.next(); // { value: "😀", done: false }iterator.next(); // { value: "B", done: false }iterator.next(); // { value: undefined, done: true }No calls yet.
Choose an iterable, then press next() to ask for one result object at a time.
The Unicode & string internals lesson explains why "😀".length can be 2. Iteration is friendlier: the string iterator yields that emoji as one value.
Consumers: who keeps pressing next()?
INTERACTIVEA consumer is anything that accepts an iterable and pulls values from it. The syntax may look different, but under the hood each consumer asks for an iterator and keeps calling next() until it has enough or sees done: true.
Some customers empty the dispenser. Some leave early. When they leave early, JavaScript politely gives the iterator a chance to clean up by calling return().
- In real life: A customer keeps pressing until the dispenser says empty
- In JavaScript:
for...of, spread, andArray.fromconsume untildone: true - In real life: A customer only needs two tickets and leaves
- In JavaScript: Destructuring
[a, b] = iterablestops early - In real life: A polite customer returns the dispenser
- In JavaScript: Early exit calls
return()if the iterator provides it
Where the analogy stops: The customer does not choose the next snack. The iterator controls order and values; the consumer only requests the next result.
function counting(limit) { let nextCalls = 0; let returnCalls = 0; return { stats: () => ({ nextCalls, returnCalls }), [Symbol.iterator]() { let value = 1; return { next() { nextCalls = nextCalls + 1; return value <= limit ? { value: value++, done: false } : { done: true }; }, return() { returnCalls = returnCalls + 1; return { done: true }; }, }; }, };}1,221destructuring produced 1,2; it called next() 2 time(s) and return() 1 time(s).
The most surprising row is destructuring. [a, b] = iterable only needs two values. If the iterator has more to give and defines return(), JavaScript closes it. A break or a thrown error inside for...of does the same kind of early close.
Iterables vs iterators: reusable machine, one-shot dispenser
INTERACTIVEOnce you tear a ticket off a roll, it is gone from that roll. To start over, you do not tape tickets back on; you ask the booth for a fresh roll.
- In real life: A roll of tickets
- In JavaScript: One iterator object
- In real life: Tearing off ticket 1, then ticket 2
- In JavaScript: Calling
next()advances the saved position - In real life: An empty roll stays empty
- In JavaScript: A done iterator does not rewind
- In real life: Ask the booth for a new roll
- In JavaScript: Call the iterable’s
[Symbol.iterator]()again
Where the analogy stops: Some custom iterators can choose unusual behavior, but built-in iterators are one-way. Treat iterators as consumable.
const iterable = ["tea", "cake"];const iterator = iterable[Symbol.iterator](); console.log(iterator.next());console.log(iterator.next());console.log(iterator.next());console.log(iterator.next()); const fresh = iterable[Symbol.iterator]();console.log(fresh.next());console.log(iterator[Symbol.iterator]() === iterator);Not run yet.
Run it, then compare the original iterator with the fresh one.
Built-in iterator objects are a useful special case: they are iterable themselves. That means iterator[Symbol.iterator]() === iterator. A consumer can accept an iterator directly, but it will consume the iterator from its current position, not from the beginning.
Built-in iterables you already use
JavaScript ships many values that already speak the protocol. Arrays, strings, Maps, Sets, typed arrays, and arguments are iterable. In browsers, DOM collections such as NodeList are iterable in modern engines too.
| Value | Default values | Example use |
|---|---|---|
| Array | Each array item | for (const item of items) |
| String | Unicode code points | [..."A😀B"] |
| Map | [key, value] pairs in insertion order | for (const [key, value] of map) |
| Set | Each unique value in insertion order | new Set(array) and for...of |
| TypedArray | Numbers from binary storage | Uint8Array values |
| Browser NodeList | Nodes in document order | document.querySelectorAll(...) in most modern browsers |
Map’s default is especially practical. A Map yields pairs, so destructuring fits naturally: for (const [key, value] of map). You met Map and Set in the Map & Set lesson; this is the protocol that lets them plug into the rest of the language.
Plain objects are not iterable by default
INTERACTIVEPlain objects are wonderful key/value bags, but they do not have a default order that for...of should treat as “the values.” That is why this code throws a real TypeError. Convert the object to an iterable list, or add your own iterator when the object has a meaningful sequence.
const plain = { a: 1, b: 2 }; for (const item of plain) { console.log(item);} for (const entry of Object.entries(plain)) { console.log(entry);}Press Try plain object to run the failing loop in your browser.
The real runtime error appears because the plain object has no [Symbol.iterator]() method.
Most of the time, Object.entries(object) is the clean fix. It gives an array of pairs, and arrays are iterable. You saw this pattern in Object.keys, values & entries and Destructuring.
Where you’ll use this
Protocol thinking helps you write flexible code. Instead of accepting only arrays, a helper can accept any iterable: arrays from one caller, Sets from another, a Map’s keys, or a custom range. It also helps you debug errors from spread, destructuring, Promise.all, and new Map because you know what method they are searching for.
function firstTwoLabels(items) { const iterator = items[Symbol.iterator](); const first = iterator.next(); const second = iterator.next(); return [first.value, second.value];} console.log(firstTwoLabels(new Set(["draft", "review", "done"])));["red", "green"]["red"][Symbol.iterator](){ name: "Ada" }{ next() { return { done: true }; } }"A😀B"new Map([["a", 1]])range(1, 3)new Set([1, 2]).values()
Sort each value by the protocol it speaks. Ask: does it have [Symbol.iterator]()? Does it have next()?
The Iterator helpers lesson comes after Generators. Helpers add methods such as mapping and filtering to iterators, but they rely on the protocol you just learned. For now, text only: remember the foundation before the convenience methods.
Common misconceptions
“Anything with values is iterable.”
No. for...of looks for [Symbol.iterator](). A plain object has values but no default iterable protocol.
“An iterable and an iterator are the same thing.”
An iterable can make an iterator. An iterator has next() and remembers progress. Some objects, especially built-in iterators, are both.
“The final value always matters.”
Once done is true, for...of stops and ignores the value on that result.
“Iterators rewind after they finish.”
Finished iterators stay finished. Ask the iterable for a new iterator to start again.
“Early exit just abandons the iterator.”
If the iterator has return(), JavaScript calls it when a consumer leaves early. That gives custom iterators a cleanup hook.
Practice: trace and build iterables
5 EXERCISESTrace the iterator result objects. What three things print?
const letters = ["a", "b"];
const iterator = letters[Symbol.iterator]();
console.log(iterator.next().value);
console.log(iterator.next().done);
console.log(iterator.next().done);The first call's value is a. The second call is not done yet, so it prints false. The third call has no more values, so its done is true.
Run or read the program. What does spreading the custom range print?
function range(start, end) {
return {
[Symbol.iterator]() {
let current = start;
return {
next() {
if (current > end) return { done: true };
return { value: current++, done: false };
},
};
},
};
}
console.log([...range(2, 4)].join(","));function range(start, end) {
return {
[Symbol.iterator]() {
let current = start;
return {
next() {
if (current > end) return { done: true };
return { value: current++, done: false };
},
};
},
};
}
console.log([...range(2, 4)].join(","));Each spread pull calls next(). The range yields 2, 3, and 4, then returns done: true, so the joined output is 2,3,4.
What value prints after new Map consumes the pair iterable?
const entries = [["theme", "dark"], ["font", "mono"]];
const settings = new Map(entries);
console.log(settings.get("font"));const entries = [["theme", "dark"], ["font", "mono"]];
const settings = new Map(entries);
console.log(settings.get("font"));The entries iterable gives new Map two pairs. Looking up font returns the value from the second pair: mono.
Change the loop so it prints name=Ada and role=admin.
const user = { name: "Ada", role: "admin" };
for (const item of user) {
console.log(item); // TypeError: user is not iterable
}const user = { name: "Ada", role: "admin" };
for (const [key, value] of Object.entries(user)) {
console.log(key + "=" + value);
}Object.entries(user) returns an iterable array of pairs. The first pair is ["name", "Ada"], so the first line prints name=Ada.
Predict the two lines printed by a for...of loop that breaks after the first value.
const iterable = {
[Symbol.iterator]() {
let value = 1;
return {
next() {
return value <= 3 ? { value: value++, done: false } : { done: true };
},
return() {
console.log("closed");
return { done: true };
},
};
},
};
for (const value of iterable) {
console.log(value);
break;
}const iterable = {
[Symbol.iterator]() {
let value = 1;
return {
next() {
return value <= 3 ? { value: value++, done: false } : { done: true };
},
return() {
console.log("closed");
return { done: true };
},
};
},
};
for (const value of iterable) {
console.log(value);
break;
}The loop prints 1, then break leaves early. IteratorClose calls return(), so closed prints next.
Quiz: check your understanding
8 QUESTIONSPredict first. Every answer explains the protocol step that makes it right or wrong.
Question 1 of 8Which method makes an object iterable?
Choose an answer to see the explanation.
Question 2 of 8What does this manual iterator code print?
Read the code, then predictconst iterator = ["x"][Symbol.iterator](); console.log(iterator.next().value); console.log(iterator.next().done);Choose an answer to see the explanation.
Question 3 of 8What does
for...ofdo with the value on a{ done: true }result?Choose an answer to see the explanation.
Question 4 of 8What does string iteration print here?
Read the code, then predictfor (const character of "A😀B") { console.log(character); }Choose an answer to see the explanation.
Question 5 of 8Which consumer calls
return()when it stops early?Choose an answer to see the explanation.
Question 6 of 8What does Map iteration yield by default?
Choose an answer to see the explanation.
Question 7 of 8What happens with this plain object loop?
Read the code, then predictconst plain = { a: 1 }; for (const item of plain) { console.log(item); }Choose an answer to see the explanation.
Question 8 of 8Which statement about built-in iterators is true?
Choose an answer to see the explanation.
Key takeaways
- An iterable has a
[Symbol.iterator]()method that returns an iterator. - An iterator has
next(), which returns{ value, done }result objects. for...of, spread,Array.from, destructuring,Promise.all, andnew Mapare iterable consumers.- Early exit calls an iterator’s
return()method if it exists. - Plain objects are not iterable by default; use
Object.entriesor define a custom iterator.
Remember the one-liner.
An iterable makes an iterator; an iterator’s next() returns { value, done }.
Up next: Generators, functions that produce iterator values with yield.