Iterators & Iterables
Reviewed & published by Brayan K
Every time you write for…of, spread an array with [...items], or destructure values, a hidden protocol is doing the work. That protocol is the iteration protocol, built on Symbol.iterator.
Part of the free JavaScript course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
Understanding it turns "magic" syntax into something you can read, debug, and even build yourself for your own data structures.
What You'll Learn in This Lesson
- The difference between an iterable and an iterator
- How for…of really works under the hood
- The Symbol.iterator method and the next() contract
- Which built-ins are iterable (and which aren't)
- Building a custom iterable from scratch
- Spread, destructuring, and Array.from with iterables
📼 Real-World Analogy: Think of a museum audio guide tour:
- • The tour (the iterable) knows how to start a walkthrough on demand
- • When you press play it hands you a guide (the iterator)
- • Each press of "next" gives you one stop (a value) plus "are we done?" info
- • When the last room is reached, the guide says done and the tour ends
🧠 The Iteration Protocol in One Picture
JavaScript splits "being loopable" into two cooperating pieces:
- An iterable is any object with a method keyed by Symbol.iterator. Calling that method returns an iterator.
- An iterator is any object with a next() method that returns { value, done } each time it is called.
That is the entire contract. Once an object honours it, for…of, spread ..., destructuring, Array.from, and Promise.all all "just work" on it.
⚙️ What for…of Actually Does
A for…of loop is not magic — it is a small, predictable recipe. When you write for (const x of thing), the engine:
- Calls thing[Symbol.iterator]() to get an iterator.
- Calls iterator.next() and reads the returned { value, done }.
- If done is false, it runs the loop body with value, then repeats step 2.
- If done is true, it stops.
Here is the same loop written both ways so you can see the equivalence:
📋 Which Built-ins Are Iterable?
| Type | Iterable? | What you get per step |
|---|---|---|
| Array | ✅ Yes | Each element |
| String | ✅ Yes | Each character (full code point) |
| Map | ✅ Yes | A [key, value] pair |
| Set | ✅ Yes | Each unique value |
| arguments | ✅ Yes | Each passed argument |
| { plain object } | ❌ No | Use Object.entries() first |
This is exactly why for (const ch of "héllo") works but for (const x of {a: 1}) throws "is not iterable".
🛠️ Building Your Own Iterable: a Range
Languages like Python have a built-in range(). JavaScript doesn't, so let's build one. We want for (const n of range(1, 5)) to print 1 through 5 without ever creating an array in memory — values are produced one at a time, on demand.
Notice we never built an array of a million numbers — range(1, 1000000) would use almost no memory because each value appears only when next() asks for it. That laziness is the real power of iterators.
✨ Everything That Consumes Iterables
Once your object is iterable, a whole family of syntax accepts it:
// 🎯 YOUR TURN — replace each ___ using the hint beside it.
const shelf = {
books: ["Dune", "Emma", "Ulysses"],
// 1) The well-known symbol that makes an object iterable.
[___.iterator]() { // 👉 replace ___ with Symbol
let i = 0;
const books = this.books;
return {
// 2) The one method an iterator must have.
___() { // 👉 replace ___ with next
return i < books.length
// 3) Each step reports the value and whether it has finished.
? { value: books[i++], ___: false } // 👉 replace ___ with done
: { value: undefined, done: true };
},
};
},
};
// 4) for...of asks the object for its iterator and walks it.
for (const title ___ shelf) console.log("Read:", title); // 👉 replace ___ with of
// 5) Spread uses the same protocol to fill an array.
console.log("Spread:", [___shelf]); // 👉 replace ___ with ...
const it = shelf[Symbol.iterator]();
console.log(it.next());
console.log(it.next());
const set = new Set(["a", "b", "a"]);
console.log("Set as array:", [...set]);
for (const [k, v] of Object.entries({ x: 1, y: 2 })) console.log(k, "=", v);
// ✅ Expected output:
// Read: Dune
// Read: Emma
// Read: Ulysses
// Spread: [ 'Dune', 'Emma', 'Ulysses' ]
// { value: 'Dune', done: false }
// { value: 'Emma', done: false }
// Set as array: [ 'a', 'b' ]
// x = 1
// y = 2🧩 Reorder Challenge
These lines build and drain an iterator, but they are scrambled. Put them in the correct order so the program logs each color and finally done.
D → B → C → E → F → A → G → H
Why: you must create the data (D) before asking it for an iterator (B). You need a first next() result (C) before the while can test done (E). Inside the loop you print the current value (F) and only then advance (A); forgetting line A is the classic infinite-loop bug because result.done would never become true.
🧠 Quick Recall
1. What does this print?
a false — the first next() returns value "a" with done: false; the second returns "b" and its done is still false because there is one more step (the empty final call) before the string is exhausted.
2. What happens here?
It throws TypeError: object is not iterable. Plain objects have no Symbol.iterator. Use Object.values(obj) or Object.entries(obj) to loop instead.
3. What is the result?
[ 1, 2, 3 ] — a Set is iterable and only stores unique values, so spreading it produces a deduplicated array. This is the idiomatic "remove duplicates" trick.
⚠️ Common Mistakes to Avoid
- 1️⃣ Forgetting to advance the iterator. Calling next() only once inside a while (!result.done) loop creates an infinite loop.
- 2️⃣ Expecting plain objects to be iterable. Reach for Object.entries() / Object.values() instead of for…of directly.
- 3️⃣ Returning a bare value from next(). It must return { value, done }, not just the value.
- 4️⃣ Reusing a drained iterator. Once done is true, an iterator is finished — call Symbol.iterator again to start over.
Lesson Complete — Iterators & Iterables!
- ✅ An iterable has Symbol.iterator; an iterator has next()
- ✅ next() returns { value, done } until done is true
- ✅ for…of, spread, destructuring, and Array.from all consume iterables
- ✅ Custom iterables produce values lazily — great for huge or infinite sequences
Practice quiz
What makes an object an iterable?
- It has a next() method
- It has a method keyed by Symbol.iterator
- It is an array
- It has a length property
Answer: It has a method keyed by Symbol.iterator. An iterable has a Symbol.iterator method that returns an iterator.
What must an iterator's next() method return each call?
- Just the value
- An object with value and done properties
- A Promise
- true or false
Answer: An object with value and done properties. next() returns { value, done }; done stays false until iteration finishes.
Why does for...of throw on a plain object like { a: 1 }?
- Objects are too large
- Plain objects have no Symbol.iterator
- for...of only works on numbers
- You must use let, not const
Answer: Plain objects have no Symbol.iterator. Plain objects do not implement Symbol.iterator, so they are not iterable. Use Object.entries/values.
What does each step of iterating a Map yield?
- Just the keys
- Just the values
- A [key, value] pair
- The whole Map
Answer: A [key, value] pair. Maps are iterable and yield a [key, value] pair on each step.
What is logged? console.log([...new Set([1, 1, 2, 3, 3])]);
- [ 1, 1, 2, 3, 3 ]
- [ 1, 2, 3 ]
- [ 3 ]
- It throws
Answer: [ 1, 2, 3 ]. A Set is iterable and stores only unique values, so spreading it deduplicates the array.
What is the first thing a for...of loop does internally?
- Calls thing[Symbol.iterator]() to get an iterator
- Reads thing.length
- Calls thing.next() directly
- Spreads thing into an array
Answer: Calls thing[Symbol.iterator]() to get an iterator. for...of first calls Symbol.iterator to obtain an iterator, then repeatedly calls next().
A custom range() iterable that yields values on demand mainly avoids what?
- Type errors
- Building a large array in memory
- Using Symbol.iterator
- Calling next()
Answer: Building a large array in memory. Lazy iterators produce each value only when asked, so range(1, 1000000) uses almost no memory.
Which of these is NOT iterable by default?
- Array
- String
- A plain object
- Set
Answer: A plain object. Arrays, strings, Maps, and Sets are iterable; a plain {} object is not.
After an iterator's done becomes true, what happens if you keep calling next()?
- It restarts from the beginning
- It stays finished — you must get a fresh iterator to start over
- It throws an error
- It returns the last value again
Answer: It stays finished — you must get a fresh iterator to start over. A drained iterator is finished; call Symbol.iterator again for a new iterator to iterate again.
Which syntax does NOT consume an iterable?
- Spread [...x]
- Array.from(x)
- Destructuring const [a] = x
- Accessing x.length
Answer: Accessing x.length. Spread, Array.from, and destructuring all consume iterables; .length is just a property, not iteration.
Continue this course
- Previous: Callbacks & Callback Hell
- Next: Generators (function*, yield) — Pause and resume functions to produce values on demand
- Quick reference: JavaScript cheat sheet
Frequently asked questions
What is the difference between an iterable and an iterator?
An iterable is any object that knows how to expose its values to a loop — it has a Symbol.iterator method that returns an iterator. The iterator is the worker that actually walks through the values: it has a next() method that hands back one value at a time. Arrays, strings, Maps and Sets are all iterables; the object their Symbol.iterator returns is the iterator.
Why does for…of work on arrays and strings but not on plain objects?
for…of only works on iterables, and a plain {} object does not implement Symbol.iterator by default. Arrays, strings, Maps, Sets, and arguments do. To loop a plain object's values use Object.values(obj), or its entries with Object.entries(obj), both of which return arrays you can iterate.
What must the next() method return?
It must return an object with two properties: value (the current item) and done (a boolean). While there are more items, done is false. When iteration is finished, return { value: undefined, done: true }. The for…of loop keeps calling next() until done becomes true.
Do I have to write Symbol.iterator by hand very often?
Rarely in everyday code, because built-in collections already implement it. You implement it yourself when you build a custom data structure (a range, a linked list, a paginated API wrapper) and want it to feel native — usable with for…of, spread, and destructuring.
What is the connection between iterators and generators?
Generators are the easiest way to produce an iterator. A generator function (function*) automatically returns an object that is both an iterator and an iterable, so you rarely write the next()/done plumbing yourself. Generators are covered in the very next lesson.