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 agreementSay how
[Symbol.iterator]()makes an iterator, and what eachnext()call gives back. - 02Predict consumersFollow how
for...of, spread, destructuring, andnew Mapread values from an iterable. - 03Build and fix iterablesWrite your own range and fix the error
TypeError: object is not iterable.
One agreement behind many tools
In the for…of & for…in lesson, you used for...of to get the values out of arrays and strings. This lesson shows how for...of gets those values. It works with two kinds of objects:
- An iterable is a value that can make an iterator. Arrays and strings are iterables.
- An iterator is an object that gives out values one at a time. It remembers which value comes next.
The rules for these two objects are called the iteration protocols. A protocol is an agreement: if an object has the right methods, JavaScript knows how to use it.
Many tools follow this one agreement. Each of these can take values from any iterable:
for...ofloops- spread, like
[...values] Array.from(values)- array destructuring, like
const [a, b] = values Promise.all(values)new Map(values)
You ask the machine for a dispenser. Then you press the dispenser once for each snack. The machine can give another customer a new dispenser. Each dispenser remembers how many snacks it has given.
- 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 to get one snack
- In JavaScript: Calling
next()to get one{ value, done }result
The table below compares the two kinds of objects. Read each row as: "this kind of object has this method, and the method gives back this."
| Kind of object | Which method must it have? | What does the method give back? | Examples |
|---|---|---|---|
| Iterable | It has a [Symbol.iterator]() method. | It gives back a new iterator. | Arrays, strings, Maps, Sets, and this lesson's range. |
| Iterator | It has a next() method. | Each call gives back one result, like { value, done }. | The object that array[Symbol.iterator]() gives back. |
| Iterable iterator | It has both methods. | Its [Symbol.iterator]() method gives back the iterator itself. | The iterators that arrays, strings, Maps, and Sets make. |
The two words sound almost the same, so read them carefully. An array is an iterable. It is not an iterator. The object that array[Symbol.iterator]() gives back is the iterator.
colors holds "red", "green", and "blue". Its [Symbol.iterator]() method makes an iterator, and the first next() call on that iterator gives { value: "red", done: false }.Symbol.iterator: the method every loop looks for
FOUNDATIONAn iterable must have one method. The method's name is not a normal string. It is the built-in symbol Symbol.iterator. A symbol is a special value that can be used as a property name. It never clashes with a normal name like "iterator". You met symbols in the Symbols lesson.
To use a symbol as a method name, put it in square brackets: [Symbol.iterator]() { ... }. Here are the smallest iterable and iterator:
const iterable = { [Symbol.iterator]() { return iterator; },}; const iterator = { next() { return { done: true }; },};- Lines 2–3: the iterable's
[Symbol.iterator]()method gives back the iterator. - Lines 8–9: the iterator's
next()method gives back a result. This one saysdone: trueright away, so it gives no values at all.
Here is the same shape doing useful work. range(2, 4) gives the numbers 2, 3, and 4.
function range(start, end) {
return {
[Symbol.iterator]() {
let current = start;
return {
next() {
return current <= end
? { value: current++, done: false }
: { done: true };
},
};
},
};
}
console.log([...range(2, 4)].join(","));Line 4 makes a variable, current, inside the [Symbol.iterator]() method. The next() method can still use current after that method has finished. (A function that keeps using a variable from around it is called a closure.) So the iterator remembers its place between calls. Each next() call on lines 7–9 does one of two things:
- If
currentis not pastend, it gives back{ value: current, done: false }. Thencurrent++adds 1 tocurrent. - Otherwise, it gives back
{ done: true }.
On the last line, spread (...) asks the range for an iterator. It calls next() until it sees done: true. The output is 2,3,4.
You can spread the same range again and get the same three numbers. Each call to [Symbol.iterator]() makes a new current that starts at start. This matters when two loops need to start from the beginning.
Most iterables work this way: they make a new iterator each time you ask. The iterable can be used many times. Each iterator it makes goes through the values only once.
JavaScript does not need a special class or parent object. If an object has a [Symbol.iterator] method that gives back an object with a next() method, every loop and tool in this lesson can use it.
JavaScript checks two things when a loop or spread starts:
[Symbol.iterator]()must give back an object.- That object must have a
next()method.
If either check fails, JavaScript throws a TypeError. Below, the method gives back the number 1 instead of an iterator.
const broken = {
[Symbol.iterator]() {
return 1;
},
};
console.log([...broken]);Spread calls the method on line 2 and gets 1. A number is not an object, so spread throws a TypeError before it reads any value.
next() and done: what each call gives back
STEP THROUGHEvery next() call gives back a small object called a result. A result has two properties:
value: the value being handed out.done:falsewhile there are values, andtruewhen there are no more.
Code that reads the values stops as soon as it sees done: true. Step through the code below. Line 1 makes a range from 1 to 5. Line 2 asks it for an iterator. Lines 4–7 call next() four times.
Predict each result before you step. Then change the step to make the same range count in a different way.
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 }; }, }; }, };}Look at the last result. for...of checks done first. If done is true, the loop body does not run. Even if that last result has a value, for...of 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.
Pick an iterable. Then press Call next() to get one result at a time.
A countdown shows the difference between the last value and the final result. The first three calls give 3, 2, and 1. The fourth call gives { value: "go", done: true }. Lines 9–11 give the countdown a [Symbol.iterator]() method that gives back the countdown itself, so spread can use it.
function countdown(start) {
let current = start;
return {
next() {
return current > 0
? { value: current--, done: false }
: { value: "go", done: true };
},
[Symbol.iterator]() {
return this;
},
};
}
console.log([...countdown(3)].join(","));The output is 3,2,1. A result with done: true may carry a value, but spread, for...of, and the other tools in this lesson ignore it.
next() four times. It keeps the three values that come with done: false and stops at done: true, so "go" never reaches the output.The Unicode & string internals lesson explains why "😀".length is 2: the emoji is stored in two pieces. The string's iterator does not split it. It gives the emoji as one value.
Consumers: who keeps calling next()?
INTERACTIVEA consumer is any tool that takes an iterable and reads values from it. Consumers look different in code, but they all follow the same steps:
- Call
[Symbol.iterator]()to get an iterator. - Call
next()again and again. - Stop when they have enough values, or when a result says
done: true.
Some customers keep pressing until the dispenser is empty. Some take a few snacks and leave. A polite customer who leaves early hands the dispenser back, so it can be tidied up.
- In real life: A customer keeps pressing until the dispenser is empty
- In JavaScript:
for...of, spread, andArray.fromread untildone: true - In real life: A customer needs only two snacks and leaves
- In JavaScript: Destructuring
[a, b] = iterablestops early - In real life: A polite customer hands the dispenser back
- In JavaScript: Stopping early calls
return(), if the iterator has it
The lab below gives each consumer the values 1, 2, and 3. new Map gets two pairs instead, ["a", 1] and ["b", 2]. The lab counts how many times each consumer calls next() and return().
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 gave 1,2. It called next() 2 time(s) and return() 1 time(s).
An iterator may also have a return() method. JavaScript calls it when a consumer stops early, so the iterator can clean up. For example, it could close a file it was reading. Below, the loop stops after the first value with break.
const stream = {
[Symbol.iterator]() {
let value = 1;
return {
next() {
return value <= 3 ? { value: value++, done: false } : { done: true };
},
return() {
console.log("cleaned up");
return { done: true };
},
};
},
};
for (const value of stream) {
console.log(value);
break;
}The output is 1, then cleaned up. The break leaves the loop while values are still left, so JavaScript calls return().
Destructuring is the most surprising consumer. const [a, b] = iterable needs only two values. After the second value, the iterator is not done yet. If it has a return() method, JavaScript calls it. A break or a thrown error inside for...of also calls return().
1, 2, and 3, but [a, b] needs only two values. After two next() calls, JavaScript calls return() to close the iterator, so 3 is never read.Iterables vs iterators: use again, or use once
INTERACTIVEOnce you tear a ticket off a roll, it is gone from that roll. To start again, you don't tape the tickets back on. You ask the booth for a new 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: Each
next()call moves the saved place forward - In real life: An empty roll stays empty
- In JavaScript: A finished iterator does not go back to the start
- In real life: Ask the booth for a new roll
- In JavaScript: Call the iterable’s
[Symbol.iterator]()again
The lab below calls next() four times on one array iterator. The array has only two items, so the last two calls give done: true. Then line 9 asks the array for a new iterator.
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 the code. Then compare the first iterator with the new one.
done: true. Line 9 asks the same array again, and the fresh iterator starts over at "tea".A consumer can also start in the middle of an iterator. Line 4 takes the first color. Line 5 spreads the same iterator, so it gets only the colors that are left.
const colors = ["red", "green", "blue"];
const iterator = colors[Symbol.iterator]();
console.log(iterator.next().value);
console.log([...iterator].join(","));The output is red, then green,blue. Spread does not go back to the array. It reads from the iterator it was given, starting where that iterator is.
This works because the iterators JavaScript makes are iterables too. Their [Symbol.iterator]() method gives back the iterator itself, so iterator[Symbol.iterator]() === iterator is true. You can pass such an iterator to any consumer. The consumer starts from the iterator's current place, not from the beginning.
Built-in iterables you already use
Many values in JavaScript are already iterables:
- arrays
- strings
- Maps and Sets
- typed arrays, which are arrays of raw numbers, like
Uint8Array arguments, the list of arguments inside a normal (non-arrow) function- in browsers, lists of page elements, like a
NodeList
The table below shows what each one gives you, one value for each next() call.
| Which iterable? | What does each next() give? | Example code |
|---|---|---|
| Array | Each item, in order. | for (const item of items) |
| String | Each character. An emoji counts as one. | [..."A😀B"] |
| Map | Each [key, value] pair, in the order it was added. | for (const [key, value] of map) |
| Set | Each value, in the order it was added. | new Set(array) and for...of |
| Typed array | Each number stored in it. | The values of a Uint8Array |
| NodeList (browser) | Each element, in page order. | document.querySelectorAll(...) |
A Map gives one pair for each entry: a two-item array, [key, value]. So you can split each pair right in the loop with destructuring: for (const [key, value] of map). You met Map and Set in the Map & Set lesson. This agreement is what lets them work with for...of and spread.
const settings = new Map([
["theme", "dark"],
["font", "mono"],
]);
for (const [key, value] of settings) {
console.log(key + "=" + value);
}The output is theme=dark, then font=mono. The Map keeps the order the entries were added. Each entry comes out as a two-item array, so line 6 can split it into key and value.
Plain objects are not iterable
INTERACTIVEA plain object, like { a: 1, b: 2 }, has no [Symbol.iterator]() method. JavaScript can't guess which values you want: the keys, the values, or both. So for...of on a plain object throws a TypeError. You can fix it in two ways:
- Turn the object into an array first, for example with
Object.entries. - Give the object its own
[Symbol.iterator]()method, when its values have a clear order.
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.
Your browser throws a real TypeError, because the plain object has no [Symbol.iterator]() method.
Most of the time, Object.entries(object) is the simple fix. It gives back an array of [key, value] pairs, and arrays are iterable. You saw it in the Object.keys, values & entries and Destructuring lessons.
const user = { name: "Asha", role: "editor" };
for (const [key, value] of Object.entries(user)) {
console.log(key + "=" + value);
}The output is name=Asha, then role=editor. Pick the method that gives you what you need:
Object.keysfor the keys.Object.valuesfor the values.Object.entriesfor both, as pairs.
Where you’ll use this
Knowing the agreement helps you write helpers that accept more kinds of input. A helper that only asks for an iterable works with an array, a Set, a Map's keys, or a custom range.
It also helps you read errors. When spread, destructuring, Promise.all, or new Map says a value "is not iterable", it means it could not find a [Symbol.iterator]() method on that value.
The helper below takes the first two values from any iterable. Line 2 asks for an iterator. Lines 3–4 call next() twice. Line 8 passes it a Set, and it gives back draft and review.
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"])));A class can follow the agreement too. Below, the constructor saves an array in this.items. The [Symbol.iterator]() method passes the job to that array: it gives back the array's own iterator. So the class needs no next() method of its own.
class ShoppingList {
constructor(items) {
this.items = items;
}
[Symbol.iterator]() {
return this.items[Symbol.iterator]();
}
}
const groceries = new ShoppingList(["tea", "rice"]);
console.log([...groceries].join(","));The output is tea,rice. You can spread groceries again, because every call to [Symbol.iterator]() makes a new array iterator. Use this for a collection whose items have a clear order. Don't make a plain object iterable just to avoid choosing between its keys and its values.
["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. Ask two questions: does it have a [Symbol.iterator]() method? Does it have a next() method?
The Iterator helpers lesson comes after the Generators lesson. Iterator helpers are methods like map and filter for iterators. They are built on the agreement you learned here.
Common misconceptions
“Anything with values is iterable.”
No. for...of looks for a [Symbol.iterator]() method. A plain object has values but no such method, so it is not iterable.
“An iterable and an iterator are the same thing.”
An iterable makes iterators. An iterator has next() and remembers its place. Some objects are both, such as the iterators that arrays and strings make.
“The final value always matters.”
Once done is true, for...of stops. It ignores any value on that last result.
“Iterators go back to the start after they finish.”
A finished iterator stays finished. To start again, ask the iterable for a new iterator.
“Stopping early just leaves the iterator behind.”
If the iterator has a return() method, JavaScript calls it when a consumer stops early. That gives the iterator a chance to clean up.
Practice: trace and build iterables
5 EXERCISESFollow the three results. 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 gives a. The second call gives b, so its done is false. The third call finds no more values, so its done is true.
Read or run the program. What does spreading the 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(","));Spread calls next() until it sees done: true. The range gives 2, 3, and 4, so the output is 2,3,4.
What prints after new Map reads the pairs?
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"));new Map reads two pairs from entries. settings.get("font") finds 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) gives back an array of pairs, and arrays are iterable. The first pair is ["name", "Ada"], so the first line is name=Ada.
This for...of loop stops with break after the first value. Predict the two lines it prints.
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, so JavaScript calls return(), which prints closed.
Quiz: check your understanding
8 QUESTIONSPredict each answer first. Every answer says why it is 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 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 this loop over a string print?
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
for...ofget from a Map?Choose an answer to see the explanation.
Question 7 of 8What happens when this loop runs on a plain object?
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 is true about the iterators that arrays, strings, Maps, and Sets make?
Choose an answer to see the explanation.
Key takeaways
- An iterable has a
[Symbol.iterator]()method. That method gives back an iterator. - An iterator has a
next()method. Each call gives back a result,{ value, done }. for...of, spread,Array.from, destructuring,Promise.all, andnew Mapread values from iterables. They are consumers.- When a consumer stops early, JavaScript calls the iterator's
return()method, if it has one. - Plain objects are not iterable. Use
Object.entries, or give the object its own[Symbol.iterator]()method.
Remember the one-liner.
An iterable makes an iterator. Each next() call on the iterator gives back { value, done }.
Up next: Generators, functions that make iterators for you with yield.