Command Palette

Search for a command to run...

Hectal
PHASE 14Advanced ES2015+ ~21 min· topic 1 of 9

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.

manual.jswhole filejavascript
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.

desugar.jswhole filejavascript
// 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);
}
See what for...of does for youdiagram
Rendering diagram…

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.

range.jswhole filejavascript
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. 1

    Loop twice

    In the Range example, add a second for (const n of r) loop that logs each n. Predict: does it print 1 to 5 again? Why does a fresh iterator make that work?

  2. 2

    Return the same iterator

    In the Range example, move let current = this.from; out of the method and into the constructor as this.current = from, and read this.current in next() (use an arrow function so this works). Predict what the second console.log([...r]) prints.

  3. 3

    Stop with an error

    In the countdown example, replace break with throw new Error("stop") inside the loop. Predict: does cleanup still print before the red error?

Code & diagrams

Driving an iterator by hand ES2015+ New tab

Once an iterator is done, it stays done. Plain objects have no `Symbol.iterator` method.

Preview

Press Run to see the page.

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? false
A Range class that works with every consumer ES2015+ New tab

One 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

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

[ 1, 2, 3, 4, 5 ]
1 2
5
[ 10, 20, 30 ]
Set(3) { 2, 3, 4 }
return() runs when a consumer stops early New tab

`break` and the one-item destructuring both stopped early, so `return()` ran. The last loop ran to the end, so nothing needed cleaning.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

5
4
3
cleanup: return() called
cleanup: return() called
a = 2
full run 2
full run 1
Strings iterate by character, not by code unit New tab

`length` counts UTF-16 code units; the string iterator steps by code point. `entries()` returns another iterator, of `[index, value]` pairs.

Preview

Press Run to see the page.

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);

terminal
$ Console
── what you'll see ──
Uncaught TypeError: user is not iterable

Break #2

Spread an iterator twice

Write const it = [1, 2, 3].values(); console.log([...it]); console.log([...it]);

terminal
$ Console
── what you'll see ──
[ 1, 2, 3 ]
[]

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]() returns this. ES2025 added a global Iterator and iterator helpers on that prototype: map, filter, take, drop, flatMap, reduce, toArray, forEach, some, every, find, plus Iterator.from(). They are lazy, so naturals().filter(isEven).take(3).toArray() works on an endless sequence (supported in current Chrome, Edge, Firefox and Safari).

  • ▸

    When for...of exits because next() itself threw, return() is not called (the iterator is assumed broken). It is called on break, return, continue to an outer label, and errors thrown by the loop body.

  • ▸

    V8 has fast paths for spreading and for...of over unmodified arrays. Replacing Array.prototype[Symbol.iterator] (never do this) disables them for every array on the page.

  • ▸

    Async iteration is the same protocol with promises: [Symbol.asyncIterator]() and next() returning a promise of { value, done }, consumed by for await...of. Readable streams and many Node.js APIs are async iterables.

Remember this

  1. 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: false means value is the next item, done: true means there are no more (the value is then usually undefined). An iterable is any object with a method stored under the special key Symbol.iterator that returns a new iterator.

  2. 2

    Symbol.iterator is 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 write for (const x of thing), the engine calls thing[Symbol.iterator]() once to get an iterator, then calls next() again and again, giving you each value, until done is true. If thing has no Symbol.iterator method, you get TypeError: thing is not iterable.

  3. 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, NodeList from querySelectorAll, and typed arrays. Plain objects {} are not iterable on purpose: should they give keys, values or pairs? You choose with Object.keys, Object.values or Object.entries, which return arrays (and arrays are iterable).

  4. 4

    Everything that consumes iterables uses the same protocol: for...of, spread [...x] and f(...x), array destructuring const [a, b] = x, Array.from(x), new Map(x), new Set(x), Promise.all(x) and yield*. So the moment your object has a working [Symbol.iterator](), all of these work with it for free.

  5. 5

    An iterator can have an optional **return()** method. If a consumer stops early (a break in for...of, an error thrown inside the loop, or destructuring that takes only the first few items), the engine calls return() 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. 6

    Iterators are usually one-shot: once done is true, 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 writes next() for you.

Explain it without notes

01

What is the difference between an iterable and an iterator?

02

Walk through what the engine does when it runs for (const x of obj).

03

When is an iterator's return() method called, and why does it exist?

04

Why are plain objects not iterable, and how do you loop over them?

Practice

01

Make an object letters with a [Symbol.iterator] that yields "a", "b", "c" (write next() by hand, no generator), and log [...letters].

02

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.