Skip to main content

What is symbol.iterator and why is it needed

Symbol.iterator - a built-in symbol in JavaScript that defines how an object produces a sequence of values for for...of, spread (...), Array.from(), and destructuring.

Theory

TL;DR

  • Symbol.iterator works like a vending machine's dispense slot: it defines the interface for getting values one at a time
  • Arrays, strings, Sets, and Maps have it built in; plain objects do not
  • Any object with [Symbol.iterator]() works in for...of, spread, and destructuring
  • The method must return an iterator: an object with next() that returns { value, done }
  • Need custom iteration order or lazy data loading? Implement it. Otherwise, use Array or Set directly

Quick example

javascript
// Plain object - no Symbol.iterator, throws TypeError const obj = { a: 1, b: 2 }; for (const v of obj) console.log(v); // TypeError: obj is not iterable // Add Symbol.iterator - now works everywhere const iterable = { data: [10, 20, 30], *[Symbol.iterator]() { // generator handles next() for you for (const v of this.data) yield v; } }; for (const v of iterable) console.log(v); // 10, 20, 30 console.log([...iterable]); // [10, 20, 30]

The generator syntax (function*) handles the next() plumbing automatically. The result is the same as writing { next: () => ({ value, done }) } by hand.

How the iteration protocol works

for...of does three things. It calls obj[Symbol.iterator]() once to get an iterator. Then it calls iterator.next() on each loop step. It stops when next() returns { done: true }.

Two separate roles exist here. The iterable is the object with [Symbol.iterator](). The iterator is what that method returns: a stateful { next() } object. Arrays are both at once because arr[Symbol.iterator]() returns the array itself with next() attached.

That separation matters in practice. The iterator is stateful and exhausts once. The iterable produces a fresh iterator on every call.

Iterable vs iterator

javascript
const arr = [1, 2, 3]; const it = arr[Symbol.iterator](); // get a fresh iterator console.log(it.next()); // { value: 1, done: false } console.log(it.next()); // { value: 2, done: false } console.log(it.next()); // { value: 3, done: false } console.log(it.next()); // { value: undefined, done: true }

When to implement Symbol.iterator

Implement it when you have a data structure that should behave like a sequence: a range, a paginated result set, a tree traversal, or a DOM wrapper. Skip it if Array or Set already fits.

A practical signal: if you keep writing getAll() methods that return arrays, and callers always iterate the result, putting [Symbol.iterator] directly on the object is cleaner.

How the engine handles this

V8 checks obj[Symbol.iterator] at the start of for...of. If the property is missing or not callable, it throws TypeError: obj is not iterable before the loop body runs at all. Once it has the iterator, it calls next() in a loop, reading value and done from each result. Generators compile to state machines in V8 bytecode: each yield saves the execution position and local variables, and next() resumes from that saved point.

The lazy nature of iterators matters here. A generator that fetches from a network page won't hit the network until next() is actually called. No preloading, no full array allocated in memory.

Common mistakes

Reusing an exhausted iterator:

javascript
const it = myIterable[Symbol.iterator](); // cached iterator, not the iterable for (const v of it) console.log(v); // 1, 2, 3 for (const v of it) console.log(v); // nothing - already exhausted

for...of on an iterable calls [Symbol.iterator]() fresh each time. But it is already an iterator. Done is done. Fix: iterate the original object, not the cached iterator.

Mutating source during iteration:

javascript
const obj = { data: [1, 2, 3], *[Symbol.iterator]() { yield* this.data; } }; for (const v of obj) { obj.data.shift(); // mutates the live array mid-loop console.log(v); // logs 1, 3 - skips 2 }

The generator reads from a live reference. Clone data first if you need to mutate during the loop.

Using sync iteration on async generators:

javascript
async function* fetchItems() { yield await Promise.resolve(1); } for (const v of fetchItems()) {} // TypeError for await (const v of fetchItems()) {} // correct

for...of expects a sync next(). Async generators need for await...of and use [Symbol.asyncIterator] internally, not Symbol.iterator.

Infinite iterator without a break:

javascript
const counter = { *[Symbol.iterator]() { let i = 0; while (true) yield i++; } }; for (const n of counter) { if (n > 10) break; // break triggers iterator.return() internally console.log(n); } // Without the break, this never terminates

Real-world usage

  • Node.js streams: Readable.from(iterable) (Node 12+) converts any custom iterator to a readable stream directly
  • Array.from: uses [Symbol.iterator] internally, same as spread
  • Destructuring: const [a, b] = myIterable calls [Symbol.iterator] under the hood
  • Promise.all / Promise.race: both accept any iterable, not just arrays
  • Custom ranges: for (const i of range(1, 100)) reads better than a manual for loop with an index

I've found [Symbol.iterator] most useful on database cursor wrappers: when a DB client returns a cursor, attaching the iterator protocol to it lets downstream code stay clean without any interface changes.

Follow-up questions

Q: What is the difference between an iterable and an iterator?
A: An iterable has [Symbol.iterator]() that returns a fresh iterator on each call. An iterator is the stateful { next() } object. Arrays are both: arr[Symbol.iterator]() returns arr itself with next() attached.

Q: How would you implement a finite range without generators?
A: const range = (end) => ({ [Symbol.iterator]() { let i = 0; return { next: () => i < end ? { value: i++, done: false } : { done: true } }; } });

Q: Why does for...of throw on plain objects but for...in works?
A: for...in uses the internal property enumeration mechanism, separate from the iteration protocol. for...of strictly requires [Symbol.iterator]. Two different specs, two different checks.

Q: What happens when you break inside for...of over a generator?
A: The engine calls iterator.return() if it exists, which triggers the generator's finally block. This lets generators clean up resources on early exit: close file handles, cancel network requests.

Q: (Senior) How does V8 compile a generator function?
A: As a state machine. Each yield splits the function body into numbered states. next() jumps to the current state via bytecode dispatch, runs until the next yield, saves locals and the state index into the GeneratorObject closure, then suspends.

Examples

Range iterator with manual next()

javascript
// Explicit next() - no generator syntax function range(start, end) { return { [Symbol.iterator]() { let current = start; return { next() { return current <= end ? { value: current++, done: false } : { value: undefined, done: true }; } }; } }; } for (const n of range(1, 5)) console.log(n); // 1 2 3 4 5 console.log([...range(1, 5)]); // [1, 2, 3, 4, 5] const [first, second] = range(10, 20); console.log(first, second); // 10 11

All three usages - for...of, spread, and destructuring - call [Symbol.iterator]() internally. The same object works in all three contexts without any extra code.

Paginated API fetch with async generator

javascript
// Each yield sends one user - caller never sees pagination async function* fetchAllUsers(baseUrl) { let page = 1; while (true) { const res = await fetch(`${baseUrl}?page=${page++}&limit=10`); const data = await res.json(); if (!data.length) return; // no more pages, done yield* data; // stream items from this page one by one } } // Process users without loading all pages into memory first for await (const user of fetchAllUsers('/api/users')) { console.log(user.name); if (user.role === 'admin') break; // exits early, no extra fetch }

This pattern works in Express SSE routes and Next.js data layers where full preloading is not acceptable. Note: async generators use Symbol.asyncIterator, not Symbol.iterator. The for await...of loop handles that automatically.

Tree traversal as an iterable class

javascript
class TreeNode { constructor(value, children = []) { this.value = value; this.children = children; } // Depth-first traversal via generator delegation *[Symbol.iterator]() { yield this.value; for (const child of this.children) { yield* child; // delegate to child's [Symbol.iterator] } } } const tree = new TreeNode(1, [ new TreeNode(2, [new TreeNode(4), new TreeNode(5)]), new TreeNode(3) ]); console.log([...tree]); // [1, 2, 4, 5, 3]

yield* delegates to another iterable: it calls child[Symbol.iterator]() and exhausts it before continuing. This is generator delegation - it composes tree traversals cleanly without manual stack management.

Short Answer

Interview ready
Premium

A concise answer to help you respond confidently on this topic during an interview.

Finished reading?