Topic 14.1
Iterators and the Iteration Protocol
In one line
An iterable is any object with a Symbol.iterator method that returns an iterator: an object whose next() hands out { value, done } results one at a time. for...of, spread, destructuring and Array.from all speak this protocol, so you can make your own objects work with them.
Think of it like this
A ticket machine at a bakery counter. You press the button and get the next ticket: 41, then 42, then 43. You never see the whole roll at once, and when the roll runs out the machine shows "empty". An iterator is that machine: each call to next() gives you the next value, and finally it says "done". An iterable is the bakery: something you can ask for a fresh ticket machine.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Protocol
- A set of rules that code agrees to follow, like "have a
next()method that returns{ value, done }". - Iterator
- An object with a
next()method that gives out values one at a time. - Iterator result
- The object
next()returns:{ value, done }. - Iterable
- An object with a
[Symbol.iterator]()method that returns a new iterator. Arrays, strings, Maps and Sets are iterables. - Symbol.iterator
- The special built-in key where an iterable keeps the method that makes its iterator.
- Consumer
- Code that reads from an iterable:
for...of, spread, destructuring,Array.from. - return() method
- An optional iterator method the engine calls when a consumer stops early, so the iterator can clean up.
- Lazy
- Working out each value only when someone asks for it, instead of all at the start.
Step by step
01Ask an array for its iterator
Arrays already follow the protocol. Calling arr[Symbol.iterator]() gives you the same iterator for...of would use. Each next() hands out one item.
const it = ["a", "b"][Symbol.iterator]();
it.next(); // { value: "a", done: false }
it.next(); // { value: "b", done: false }
it.next(); // { value: undefined, done: true }02See what for...of does for you
A for...of loop is a short way to write the loop below. Knowing this explains every iteration error you'll ever see: no Symbol.iterator means "is not iterable", and a next that isn't a function or returns a non-object is also a TypeError.
// for (const x of iterable) { body(x); } is roughly:
const iterator = iterable[Symbol.iterator]();
while (true) {
const result = iterator.next();
if (result.done) break;
const x = result.value;
body(x);
}03Make your own object iterable
Give the object a method under the key [Symbol.iterator] (the square brackets make it a computed key). It returns an object with next(). The variables current and last live in a closure (Topic 3.6), so each call to [Symbol.iterator]() starts a fresh count.
const range = {
from: 1,
to: 3,
[Symbol.iterator]() {
let current = this.from;
const last = this.to;
return {
next() {
return current <= last
? { value: current++, done: false }
: { value: undefined, done: true };
},
};
},
};
console.log([...range]); // [ 1, 2, 3 ]04Add return() for early exits
If your iterator holds something that needs closing, add return(). A break, a thrown error, or destructuring that stops early all call it. It must return an object (by convention { done: true }).
05Know which consumers use it
for...of, [...x], fn(...x), const [a] = x, Array.from(x), new Set(x), new Map(x), Promise.all(x) and yield* x all call [Symbol.iterator](). Object spread {...x} does not: it copies own properties instead.
Try it yourself
- 1
Loop twice
In the Range example, add a second
for (const n of r)loop that logs eachn. Predict: does it print 1 to 5 again? Why does a fresh iterator make that work? - 2
Return the same iterator
In the Range example, move
let current = this.from;out of the method and into the constructor asthis.current = from, and readthis.currentinnext()(use an arrow function sothisworks). Predict what the secondconsole.log([...r])prints. - 3
Stop with an error
In the countdown example, replace
breakwiththrow new Error("stop")inside the loop. Predict: doescleanupstill print before the red error?
Code & diagrams
Once an iterator is done, it stays done. Plain objects have no `Symbol.iterator` method.
Preview
Console
Nothing logged yet.Expected console output
{ value: 'apple', done: false }
{ value: 'banana', done: false }
{ value: undefined, done: true }
{ value: undefined, done: true }
Array iterable? true
Object iterable? falseOne method, and `for...of`, spread, destructuring, `Array.from` and `new Set` all work. Each consumer gets a fresh iterator, so `r` can be used again and again.
Preview
Console
Nothing logged yet.Expected console output
[ 1, 2, 3, 4, 5 ]
1 2
5
[ 10, 20, 30 ]
Set(3) { 2, 3, 4 }`break` and the one-item destructuring both stopped early, so `return()` ran. The last loop ran to the end, so nothing needed cleaning.
Preview
Console
Nothing logged yet.Expected console output
5
4
3
cleanup: return() called
cleanup: return() called
a = 2
full run 2
full run 1`length` counts UTF-16 code units; the string iterator steps by code point. `entries()` returns another iterator, of `[index, value]` pairs.
Preview
Console
Nothing logged yet.Expected console output
4
3
[ 'h', 'i', '😀' ]
0 h
1 i
2 😀Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Loop over a plain object
Write const user = { name: "Asha", age: 12 }; for (const x of user) console.log(x);
Break #2
Spread an iterator twice
Write const it = [1, 2, 3].values(); console.log([...it]); console.log([...it]);
Myth vs fact
Myth
Iterable and iterator mean the same thing.
Fact
An iterable makes iterators (it has [Symbol.iterator]()); an iterator hands out values (it has next()). An array is iterable but not an iterator: it has no next method.
Myth
Object spread {...obj} uses the iteration protocol.
Fact
Only array spread and argument spread do. Object spread copies own enumerable properties and works on any object.
Myth
for...of is slow compared with a for loop.
Fact
Engines optimise for...of over arrays heavily; in real code the difference is rarely measurable. Choose by readability, then measure if a hot loop matters.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
Built-in iterators all inherit from
%IteratorPrototype%, whose[Symbol.iterator]()returnsthis. ES2025 added a globalIteratorand iterator helpers on that prototype:map,filter,take,drop,flatMap,reduce,toArray,forEach,some,every,find, plusIterator.from(). They are lazy, sonaturals().filter(isEven).take(3).toArray()works on an endless sequence (supported in current Chrome, Edge, Firefox and Safari). - ▸
When
for...ofexits becausenext()itself threw,return()is not called (the iterator is assumed broken). It is called onbreak,return,continueto an outer label, and errors thrown by the loop body. - ▸
V8 has fast paths for spreading and
for...ofover unmodified arrays. ReplacingArray.prototype[Symbol.iterator](never do this) disables them for every array on the page. - ▸
Async iteration is the same protocol with promises:
[Symbol.asyncIterator]()andnext()returning a promise of{ value, done }, consumed byfor await...of. Readable streams and many Node.js APIs are async iterables.
Remember this
- 1
The iteration protocol is a pair of agreements (a protocol is a set of rules two pieces of code agree on, not a class you inherit from). An iterator is any object with a
next()method. Each call returns an iterator result, a small object{ value, done }:done: falsemeansvalueis the next item,done: truemeans there are no more (thevalueis then usuallyundefined). An iterable is any object with a method stored under the special keySymbol.iteratorthat returns a new iterator. - 2
Symbol.iteratoris a well-known symbol: a built-in, unique key the language itself looks for (you'll meet symbols fully in Topic 14.5). When you writefor (const x of thing), the engine callsthing[Symbol.iterator]()once to get an iterator, then callsnext()again and again, giving you eachvalue, untildoneistrue. Ifthinghas noSymbol.iteratormethod, you getTypeError: thing is not iterable. - 3
Many built-ins are iterable: arrays, strings (they step through characters by code point, so an emoji counts as one item, see Topic 4.2), Map, Set,
arguments,NodeListfromquerySelectorAll, and typed arrays. Plain objects{}are not iterable on purpose: should they give keys, values or pairs? You choose withObject.keys,Object.valuesorObject.entries, which return arrays (and arrays are iterable). - 4
Everything that consumes iterables uses the same protocol:
for...of, spread[...x]andf(...x), array destructuringconst [a, b] = x,Array.from(x),new Map(x),new Set(x),Promise.all(x)andyield*. So the moment your object has a working[Symbol.iterator](), all of these work with it for free. - 5
An iterator can have an optional **
return()** method. If a consumer stops early (abreakinfor...of, an error thrown inside the loop, or destructuring that takes only the first few items), the engine callsreturn()so the iterator can clean up, for example closing a file or a connection. When the loop runs to the end,return()is not called because there is nothing left to clean. - 6
Iterators are usually one-shot: once
doneistrue, they stay done. An iterable like an array gives a fresh iterator every time you ask, so you can loop over it twice. A bare iterator that returns itself from[Symbol.iterator]()(as built-in iterators do) is used up after one loop. The cleanest way to write iterators by hand is a generator (Topic 14.2), which writesnext()for you.
Explain it without notes
What is the difference between an iterable and an iterator?
Walk through what the engine does when it runs for (const x of obj).
When is an iterator's return() method called, and why does it exist?
Why are plain objects not iterable, and how do you loop over them?
Practice
Make an object letters with a [Symbol.iterator] that yields "a", "b", "c" (write next() by hand, no generator), and log [...letters].
Write take(iterable, n) that returns an array of the first n values of any iterable, and closes it properly. Test it with take("hello", 2).
Trade-offs
- ↔
Making a class iterable gives it every consumer for free and keeps memory flat (values are produced on demand); returning an array is simpler to debug and can be indexed and reused, but builds everything up front.
- ↔
Hand-written
next()methods give full control but are easy to get wrong; generators (Topic 14.2) write the same protocol in a few lines.
Done when you can
I can explain the difference between an iterable and an iterator.
I can make my own object or class iterable with
[Symbol.iterator]().I can list the consumers that use the protocol (and know object spread isn't one).
I know when
return()is called and what it's for.I know why plain objects aren't iterable and how to loop over them.