In JavaScript, what makes a value iterable, and what must the object returned by its Symbol.iterator method look like?
answer
- two contracts, not one
- a method under a symbol key
- returns an object each step
- { value, done } result record
- done true ends the loop
basics
~20 sA 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 sJavaScript 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
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.
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.
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.
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