skip to content

Iteration Protocols and Symbol.iterator

An iterable is any object with a Symbol.iterator method returning an iterator, and an iterator is any object with next() returning {value, done}. Once you see that, it becomes obvious why spread works on strings, Maps, and DOM node lists but not on plain objects.

part ofJavaScriptoverview, primer and where to startread it →
on this pageshow

questions

5

In JavaScript, what makes a value iterable, and what must the object returned by its Symbol.iterator method look like?

level: juniorimportance: must knowfreq 70%

answer

  1. two contracts, not one
  2. a method under a symbol key
  3. returns an object each step
  4. { value, done } result record
  5. done true ends the loop

basics

~20 s

A value is iterable if it has a Symbol.iterator method that returns an iterator — an object whose next() method returns { value, done }. Arrays, strings, Map and Set have one; plain objects do not.

solid answer

~40 s

JavaScript defines two separate contracts. The **iterable protocol**: an object is iterable if it has a method under the well-known symbol key `Symbol.iterator` that takes no arguments and returns an iterator. The **iterator protocol**: an object is an iterator if it has a `next()` method returning a result object with `done` (a boolean) and `value`. A consumer such as `for...of`, array spread, array destructuring or `Array.from` calls `obj[Symbol.iterator]()` once, then calls `next()` repeatedly until it gets `done: true`, using each `value` along the way. Arrays, strings, `Map`, `Set`, typed arrays and the `arguments` object all carry a built-in `Symbol.iterator`; `Object.prototype` does not, which is exactly why a plain object literal cannot be spread into an array. Anything you write yourself becomes iterable the moment you add that one method.

go deeper

for a junior

Be able to say plainly that an iterable has a Symbol.iterator method and an iterator has next() returning { value, done }, and to name arrays, strings, Map and Set as iterable while plain objects are not.

for a middle

Explain the pump: a consumer calls Symbol.iterator once, then next() until done is true. Know that a missing done reads as falsy, that the result must be an object, and that the final value is discarded by for...of.

for a senior

Show judgment about where iterator state lives and what fresh-versus-shared cursors mean for code that loops a value twice, and be ready to retrofit the protocol onto an existing type so it works with every consumer at once.

for a principal

Own the API decision: exposing a sequence as an iterable rather than a materialised array changes memory profile, re-consumption semantics and what callers can assume. Be ready to argue when that flexibility is worth the extra contract.

## Two protocols, not one Almost every confusion in this area comes from collapsing two different contracts into one word. JavaScript defines them separately. **The iterable protocol.** A value is *iterable* if it — or something on its prototype chain — has a method stored under the key `Symbol.iterator`. That method takes no arguments and must return an iterator. `Symbol.iterator` is a *well-known symbol*, a unique value exposed as a property of the `Symbol` function; it is not the string `"iterator"`, so you always write it as a computed key: `[Symbol.iterator]() { ... }`. **The iterator protocol.** A value is an *iterator* if it has a `next()` method that returns an object — the *iterator result* — with two properties: `done`, checked for truthiness, and `value`, the item produced. An object may be one, the other, or both. ## What a consumer actually does Every language construct that walks a sequence performs the same two steps: get an iterator, then pump it. In rough JavaScript, `for (const x of obj)` is: ```js const it = obj[Symbol.iterator](); let step; while (!(step = it.next()).done) { const x = step.value; // loop body } ``` The consumers that do this include `for...of`, array spread `[...obj]`, argument spread `f(...obj)`, array destructuring `const [a, b] = obj`, `Array.from(obj)`, and the constructors `new Map(obj)`, `new Set(obj)`, plus `Promise.all` and friends, which take an iterable of promises rather than specifically an array. If `obj[Symbol.iterator]` is missing or is not callable, the consumer throws a `TypeError` before any looping happens — the familiar "x is not iterable" message. ## The result object in detail - `done` falsy (including missing, which reads as `undefined`) means "this is a real element" and `value` is that element. - `done: true` means iteration is finished. Its `value` is the iterator's *return value*, and `for...of`, spread, destructuring and `Array.from` all discard it. You only observe it by calling `next()` by hand. - Both properties are optional in the sense that missing ones read as `undefined`: `{ done: true }` is a valid "finished" result, and `{ value: 5 }` is a valid "here is 5, keep going" result because `done` is `undefined`. - `next()` must return an *object*. Returning the bare value, or a number, makes the consumer throw a `TypeError`. Driving it manually makes the shape concrete: ```js const it = ['a', 'b'][Symbol.iterator](); it.next(); // { value: 'a', done: false } it.next(); // { value: 'b', done: false } it.next(); // { value: undefined, done: true } ``` ## Who ships an iterator, and who does not Built-ins with a `Symbol.iterator` method include `Array.prototype`, `String.prototype`, `Map.prototype`, `Set.prototype`, the typed-array prototypes, and the `arguments` object. The `keys()`, `values()` and `entries()` methods of arrays, `Map` and `Set` return iterator objects. `Object.prototype` has no `Symbol.iterator`. That single fact explains a whole family of errors: `[...user]` throws for a plain object, `const [a] = user` throws, `Array.from(user)` returns `[]` unless the object is array-like, and `for (const x of user)` throws. Enumerating an object's own properties is a different mechanism entirely, reached through `Object.keys`, `Object.values` or `Object.entries` — each of which hands you an array, which *is* iterable. ## Built-in iterators are usually iterable too All built-in iterator objects inherit from a shared hidden prototype, `%IteratorPrototype%`, whose `[Symbol.iterator]()` simply returns `this`. That is why you can write `for (const v of map.values())` even though `map.values()` is an iterator rather than a collection: it answers the iterable protocol by handing back itself. The consequence is that such an object carries a single, shared cursor — a detail worth remembering when a value is looped more than once. ## Making your own value iterable The minimum is one method: ```js const twoThings = { [Symbol.iterator]() { const data = ['x', 'y']; let i = 0; return { next: () => i < data.length ? { value: data[i++], done: false } : { value: undefined, done: true } }; } }; [...twoThings]; // ['x', 'y'] ``` Because the counter lives inside the method call, each consumer that asks for an iterator gets a fresh, independent cursor — the same behaviour arrays give you. Adding this one method retrofits your type into every consumer in the language at once, which is the real payoff of the protocol being so small.

  • When done is true, does the value property mean anything?
    Yes — it is the iterator's return value, not an element. `for...of`, spread, destructuring and `Array.from` all discard it, so you only see it if you call `next()` yourself and read the final result. Most hand-written iterators simply leave it `undefined`.
  • Is every iterator also iterable?
    Not by definition, but in practice usually yes. Built-in iterators inherit from `%IteratorPrototype%`, whose `[Symbol.iterator]()` returns `this`, so they satisfy both protocols. A hand-written iterator that only defines `next()` cannot be used with `for...of` until you add `[Symbol.iterator]() { return this; }`.
  • What happens if next() returns something that is not an object?
    The consumer throws a `TypeError`. The specification requires each iterator result to be an Object so that `done` and `value` can be read from it, so returning the bare element instead of `{ value, done }` fails immediately rather than silently misbehaving.

saying these in an interview costs you the question

  • Says any object with a length property is iterable
  • Confuses the iterable with the iterator itself
  • Thinks for...of reads numeric indexes directly
  • Claims plain objects are iterable because Object.keys works
  • Writes obj.iterator or obj['iterator'] instead of the symbol key

context

open as a page

Why does [...user] throw a TypeError when user is a plain object, while {...user} copies its properties without complaint?

level: middleimportance: must knowfreq 60%

basics

~20 s

Array spread runs the iteration protocol and a plain object has no Symbol.iterator method, so it throws. Object spread does not iterate at all — it copies the source's own enumerable properties onto the new object.

open as a page

Given the same argument x, what can Array.from(x) do that the array spread [...x] cannot?

level: middleimportance: should knowfreq 45%

basics

~10 s

Array.from also accepts array-likes — objects with a length and indexed properties but no Symbol.iterator — and takes an optional mapping function as its second argument. Array spread only accepts iterables and cannot map.

open as a page

How do you make instances of your own JavaScript class work with for...of and array spread, without using a generator function?

level: middleimportance: should knowfreq 50%

basics

~20 s

Add one method to the class, keyed with the computed name [Symbol.iterator], that returns a fresh iterator object each call — an object whose next() returns { value, done } and whose loop state lives in local variables.

open as a page

A JavaScript array can be looped with for...of repeatedly, but the object returned by a Map's values() method yields nothing on a second pass. Why?

level: seniorimportance: should knowfreq 35%

basics

~10 s

An array's Symbol.iterator returns a brand-new iterator on every call, so each loop starts fresh. A Map iterator answers Symbol.iterator by returning itself, so a second loop resumes the same already-exhausted cursor.

open as a page