Iterators and the Iterable Protocol
Symbol.iterator, next(), and for...of — how iterables work, custom iterators, and the difference from arrays.
- javascript
- iterators
- iterable
- symbol
for...of, spread, and destructuring all speak the iterable protocol. An object is iterable if it has a Symbol.iterator method that returns an iterator. An iterator has next() → { value, done }. Arrays, Maps, Sets, strings, and NodeLists (modern) implement this. Knowing the protocol lets you build lazy collections and understand generators.
The two protocols
const arr = [10, 20];
const iterator = arr[Symbol.iterator]();
iterator.next(); // { value: 10, done: false }
iterator.next(); // { value: 20, done: false }
iterator.next(); // { value: undefined, done: true }
// iterable: has Symbol.iterator
// iterator: has next()
// many iterators are also iterable (return self) so for...of works twice carefully
for…of under the hood
for (const x of iterable) {
// ...
}
// roughly:
const it = iterable[Symbol.iterator]();
let r = it.next();
while (!r.done) {
const x = r.value;
// body
r = it.next();
}
// if break/throw: it.return?.()
for...in is different — it walks keys (enumerable props), not iterator values.
Custom iterable
const range = {
from: 1,
to: 3,
[Symbol.iterator]() {
let n = this.from;
const last = this.to;
return {
next() {
if (n <= last) return { value: n++, done: false };
return { value: undefined, done: true };
},
};
},
};
[...range]; // [1, 2, 3]
With a generator (cleaner):
const range2 = {
from: 1,
to: 3,
*[Symbol.iterator]() {
for (let n = this.from; n <= this.to; n++) yield n;
},
};
Built-in iterables
for (const ch of 'hi') {
/* h, i */
}
for (const [k, v] of map) {
/* ... */
}
for (const v of set) {
/* ... */
}
for (const node of document.querySelectorAll('div')) {
/* NodeList is iterable */
}
Objects are not iterable by default:
// for (const x of { a: 1 }) // TypeError
for (const [k, v] of Object.entries({ a: 1 })) {
/* ... */
}
Iterator helpers (modern)
// stage / shipping in newer engines — map/filter on iterators
// Iterator.from(obj).map(fn).take(5).toArray()
Check support before relying on them; generators + functions still work everywhere.
Infinite iterables
function* forever() {
let i = 0;
for (;;) yield i++;
}
// never spread infinite iterables
// [...forever()] // 💥
Consume with break, take, or manual next.
return() and closing
const it = {
[Symbol.iterator]() {
return this;
},
next() {
return { value: 1, done: false };
},
return() {
console.log('cleaned');
return { done: true };
},
};
for (const x of it) {
break; // logs cleaned
}
Implement return when your iterator holds resources.
Interview answer
“An iterable implements Symbol.iterator returning an iterator with next() that yields {value, done}. for…of, spread, and array destructuring use that protocol. Arrays, Maps, Sets, and strings are iterable; plain objects are not. Generators make custom iterables easy. for…in is unrelated—it enumerates keys.”
Related
Consuming partial iterators
function first(iterable) {
const it = iterable[Symbol.iterator]();
const { value, done } = it.next();
it.return?.(); // close if we stop early
return done ? undefined : value;
}
function* take(n, iterable) {
let i = 0;
for (const x of iterable) {
yield x;
if (++i >= n) break; // triggers return() on generator iterators
}
}
Early exit should close resources — for...of does this on break. Manual next() loops should call return() when abandoning an iterator that holds handles. That’s part of being a good citizen of the protocol, not just implementing next.
Further reading
Related guides
- Generatorsfunction* and yield — lazy sequences, custom iterators, two-way next(value), and how generators power async patterns.
- AbortControllerCancel fetch and other async work with AbortController and AbortSignal — timeouts, race conditions, and cleanup when components unmount.
- Array find, some, every, includesShort-circuiting array predicates: find, findIndex, some, every, and includes — when to use each and common interview traps.
- Array map, filter, reducemap, filter, and reduce as the core transform toolkit — immutability, chaining costs, reduce patterns, and when a plain loop is clearer.
- Arrow Functions Deep DiveLexical this, no arguments object, no construct, concise bodies — when arrows help and when methods and generators need classic functions.