JavaScript Symbol.iterator
Every time you write for...of, use the spread operator [...iterable], or destructure an array, JavaScript quietly asks one question: does this object know how to hand out its values one at a time? The answer comes from a special property key named Symbol.iterator. Any object that has a method stored under that key is called an iterable, and every built-in language feature that loops over values checks for it. Once you understand Symbol.iterator, you can make your own classes and data structures work seamlessly with for...of, spread syntax, Array.from(), destructuring, and more.
Overview: The Iteration Protocol
JavaScript actually defines two related but distinct protocols that work together, and the terminology trips a lot of learners up.
The iterable protocol says: an object is iterable if it has a method at the key Symbol.iterator, and calling that method with no arguments returns an iterator. The iterator protocol says: an iterator is an object with a next() method that, on each call, returns a plain object shaped like { value, done }, where value is the next item and done is a boolean that becomes true once there are no more items to produce.
Arrays, strings, Map, Set, typed arrays, the arguments object, and (in browsers) NodeList are all built-in iterables — each one has a working Symbol.iterator method already wired up by the engine. Plain object literals, however, are not iterable by default. Writing for (const x of { a: 1 }) throws a TypeError because a plain object has no Symbol.iterator method — this is one of the most common surprises for newcomers.
Why a Symbol instead of a string key?
Symbol.iterator is one of JavaScript’s well-known symbols — a special, globally unique value the engine itself creates and reserves. If the language had instead used a plain string like "iterator" as the magic method name, any object that happened to already have its own iterator property (for entirely unrelated reasons) would accidentally become iterable, or worse, break in confusing ways. Because symbols are guaranteed unique, using Symbol.iterator as a property key can never collide with a string property you defined yourself.
What for…of actually does internally
A loop like for (const x of collection) { ... } is essentially syntactic sugar. Internally the engine performs roughly these steps: call collection[Symbol.iterator]() once to obtain an iterator object; repeatedly call iterator.next(); each time, check the returned object’s done property — if false, assign value to the loop variable and run the loop body, then call next() again; if true, stop looping. If the loop body exits early — via break, a return statement, or a thrown exception — the engine calls the iterator’s optional return() method (if it has one), giving the iterator a chance to release resources or run cleanup code. This is exactly why a finally block inside a generator function still runs when a consuming for...of loop breaks early.
Generator functions (covered elsewhere in this section) are special because the object they return is both an iterator (it has next()) and an iterable (its own Symbol.iterator method just returns itself). That is why you can pass a generator’s return value straight to for...of or spread it directly.
Syntax
The general form for adding Symbol.iterator to a class or object is a computed method key:
class MyClass {
[Symbol.iterator]() {
return {
next() {
return { value: /* ... */, done: /* boolean */ };
},
return(value) {
// optional: runs on early exit (break, return, throw)
return { value, done: true };
}
};
}
}
Because writing a manual next() by hand is easy to get wrong, JavaScript also lets you define Symbol.iterator as a generator method using the * shorthand — the engine builds the correct iterator object for you automatically:
class MyClass {
*[Symbol.iterator]() {
yield firstValue;
yield secondValue;
}
}
| Part | Meaning |
|---|---|
Symbol.iterator |
The well-known symbol used as the property key; marks the method that makes an object iterable. |
| The method itself | Takes no arguments and must return an object implementing the iterator protocol. |
next() |
Called repeatedly by consumers; must return { value, done } each time. |
done |
Boolean; false while more values remain, true once iteration is finished. |
value |
The item produced by this step (ignored by most consumers once done is true). |
return(value) |
Optional; called automatically on early exit so the iterator can clean up. |
Examples
Example 1: A custom Range iterable
Here is a hand-written iterator, without generators, so you can see every moving part of the protocol.
class Range {
constructor(start, end, step = 1) {
this.start = start;
this.end = end;
this.step = step;
}
[Symbol.iterator]() {
let current = this.start;
const end = this.end;
const step = this.step;
return {
next() {
if (current < end) {
const value = current;
current += step;
return { value, done: false };
}
return { value: undefined, done: true };
}
};
}
}
const range = new Range(1, 10, 2);
for (const num of range) {
console.log(num);
}
console.log([...range]);
Output:
1
3
5
7
9
[ 1, 3, 5, 7, 9 ]
Every time [Symbol.iterator]() is called it returns a brand-new iterator object with its own private current variable (captured by closure). That’s important: it means two separate for...of loops over the same range instance run independently instead of interfering with each other, because [...range] triggers a second, completely fresh call to [Symbol.iterator]().
Example 2: Built-ins already implement Symbol.iterator
You rarely need to call [Symbol.iterator]() manually, but doing so once is the best way to see the protocol in the raw.
const letters = ["a", "b", "c"];
const iterator = letters[Symbol.iterator]();
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());
console.log(typeof letters[Symbol.iterator]);
console.log(typeof "hello"[Symbol.iterator]);
console.log(typeof (new Map())[Symbol.iterator]);
Output:
{ value: 'a', done: false }
{ value: 'b', done: false }
{ value: 'c', done: false }
{ value: undefined, done: true }
function
function
function
Arrays, strings, and Map all expose a working Symbol.iterator function. Notice that once done becomes true, calling next() again on a finished array iterator keeps returning { value: undefined, done: true } rather than throwing — well-behaved iterators stay exhausted instead of erroring.
Example 3: A linked list using generator shorthand
This is the pattern you’ll use most often in real code — a generator method instead of a hand-rolled next().
class LinkedList {
constructor() {
this.head = null;
}
add(value) {
const node = { value, next: null };
if (!this.head) {
this.head = node;
} else {
let current = this.head;
while (current.next) {
current = current.next;
}
current.next = node;
}
return this;
}
*[Symbol.iterator]() {
let current = this.head;
while (current) {
yield current.value;
current = current.next;
}
}
}
const list = new LinkedList();
list.add(10).add(20).add(30);
for (const value of list) {
console.log(value);
}
console.log(Array.from(list));
console.log(Math.max(...list));
Output:
10
20
30
[ 10, 20, 30 ]
30
The generator function handles building the correct { value, done } objects and even the automatic return() cleanup behavior for you. Because LinkedList is now iterable, it works with for...of, Array.from(), and spread syntax without any extra code — anything in JavaScript that consumes iterables will accept it.
Common Mistakes
Mistake 1: Assuming every object works with for…of. Plain object literals do not implement Symbol.iterator. Code like for (const x of { a: 1, b: 2 }) throws TypeError: {...} is not iterable. If you want to loop over an object’s data, use Object.entries(obj), Object.keys(obj), or Object.values(obj) — each of those returns a real array, which is iterable — or give the object its own Symbol.iterator method:
const collection = {
items: ["x", "y", "z"],
[Symbol.iterator]() {
return this.items[Symbol.iterator]();
}
};
for (const item of collection) {
console.log(item);
}
console.log([...collection]);
Output:
x
y
z
[ 'x', 'y', 'z' ]
Mistake 2: Returning the raw value instead of a { value, done } object. A hand-written next() must always return an object with both properties — returning just the value (e.g. next() { return current++; }) breaks the protocol; consumers like for...of will read undefined for .value and undefined (falsy but not strictly checked) for .done, producing broken or infinite-looking iteration. Always shape the return value as { value: something, done: trueOrFalse }, exactly as shown in Example 1, or prefer the generator shorthand so the engine builds the shape for you automatically.
Mistake 3: Putting next() directly on the object instead of inside the method Symbol.iterator returns. Symbol.iterator must be a factory — a method that, when called, returns the iterator object. Defining next() as a sibling method on the same object rather than on the object returned from [Symbol.iterator]() means the returned iterator has no next at all, and consumers throw a TypeError when they try to call it.
Best Practices
- Prefer the generator shorthand
*[Symbol.iterator]() { yield ...; }over hand-writingnext()— it is far less error-prone and automatically supports early-exit cleanup. - Make
[Symbol.iterator]()return a fresh iterator object on every call (as in Example 1) so multiple simultaneousfor...ofloops over the same instance don’t share or corrupt state. - When your iterable simply wraps an existing iterable (an array, a
Map, another custom class), delegate withyield*or by returning the inner collection’s own iterator, instead of re-implementing iteration from scratch. - Implement the optional
return()method (or just use a generator, which gets one for free) whenever your iterator holds a resource — an open file, a timer, a lock — that must be released if the loop exits early viabreakor a thrown error. - Remember plain objects are not iterable by default; reach for
Object.entries()/keys()/values(), or add your ownSymbol.iteratorwhen you truly need an object instance to work withfor...of. - Use
typeof obj[Symbol.iterator] === "function"as the standard way to feature-detect whether a value is iterable before looping over it.
Practice Exercises
Exercise 1: Write a Fibonacci class whose constructor takes a count n, and implement [Symbol.iterator]() (using the generator shorthand) so that [...new Fibonacci(6)] produces [0, 1, 1, 2, 3, 5].
Exercise 2: Write a standalone function isIterable(value) that returns true if the given value has a callable Symbol.iterator method, and false otherwise. Test it against an array, a plain object, a string, and a number.
Exercise 3: Given a Matrix class that stores a 2D array internally (for example, rows of numbers), implement [Symbol.iterator]() so that for (const row of matrix) yields each row in order, one array at a time.
Summary
Symbol.iteratoris a well-known symbol used as a property key to mark a method that makes an object iterable.- Calling
obj[Symbol.iterator]()must return an iterator object with anext()method that yields{ value, done }pairs. for...of, spread syntax, destructuring,Array.from(), and other language features all rely on this same protocol internally.- Arrays, strings,
Map, andSetare iterable out of the box; plain objects are not. - Generator methods (
*[Symbol.iterator]() { yield ...; }) are the safest, least error-prone way to implement custom iteration. - An optional
return()method lets an iterator clean up when a loop exits early viabreak,return, or a thrown error.
