Iterators & Generators
The iteration protocol, custom iterables, generator functions, and lazy sequences in JavaScript.
Leaving a for...of loop early, with break, return or a thrown error, doesn't just stop the loop. It calls the iterator's return() method, and for a generator that means "finished for good". So if you pass the same generator to a helper like take() twice, the second call gets nothing, because the first call's break already closed it.
function* naturals(start = 0) {
let n = start;
while (true) {
yield n++;
}
}
function take(n, iterable) {
const result = [];
for (const val of iterable) {
result.push(val);
if (result.length >= n) break; // break also CLOSES the generator
}
return result;
}
const ids = naturals(1);
console.log(take(3, ids)); // first page
console.log(take(3, ids)); // second page?
// [ 1, 2, 3 ]
// []Iterators, iterables, and generator functions
You've used for...of loops to iterate over arrays and strings.
But how does JavaScript know how to loop over them? The answer is the
iteration protocol — a contract that any object can implement to
become "loopable." Generator functions make implementing this contract easy.
The iteration protocol
An object is iterable if it has a [Symbol.iterator]()
method that returns an iterator — an object with a next()
method that returns { value, done } pairs.
// Arrays are built-in iterables:
const arr = [10, 20, 30];
// for...of uses the iterator protocol under the hood:
for (const n of arr) {
console.log(n); // 10, 20, 30
}
// You can call the protocol manually:
const iter = arr[Symbol.iterator]();
console.log(iter.next()); // { value: 10, done: false }
console.log(iter.next()); // { value: 20, done: false }
console.log(iter.next()); // { value: 30, done: false }
console.log(iter.next()); // { value: undefined, done: true }Strings, Maps, Sets, NodeLists, and arguments objects are all built-in iterables.
That's why for...of, spread (...), destructuring, and
Array.from() all work on them.
Generator functions — the easy way to make iterables
Writing the iterator protocol by hand is verbose. Generator functions
(function*) do it for you. A generator can yield a value
and then pause — it resumes on the next .next() call.
// function* marks a generator function
function* countdown(from) {
while (from > 0) {
yield from; // ← pause here, return 'from', wait for next .next()
from--;
}
yield "Go!";
}
// A generator is automatically an iterable:
for (const value of countdown(3)) {
console.log(value); // 3, 2, 1, Go!
}
// Spread works too:
console.log([...countdown(3)]); // [3, 2, 1, "Go!"]A range generator — the most useful first generator
// Python has range(). JS doesn't — but you can write it:
function* range(start, end, step = 1) {
for (let i = start; i <= end; i += step) {
yield i;
}
}
for (const n of range(1, 5)) {
console.log(n); // 1, 2, 3, 4, 5
}
console.log([...range(0, 10, 2)]); // [0, 2, 4, 6, 8, 10]
// Works in destructuring:
const [first, second, third] = range(10, 50, 10);
console.log(first, second, third); // 10 20 30Infinite generators — safe because they're lazy
Generators produce values on demand — they don't calculate everything upfront. This makes infinite sequences safe (as long as you stop asking eventually):
// Infinite sequence — never ends, but that's fine:
function* naturals(start = 0) {
let n = start;
while (true) {
yield n++;
}
}
// Only take what you need:
function take(n, iterable) {
const result = [];
for (const val of iterable) {
result.push(val);
if (result.length >= n) break;
}
return result;
}
console.log(take(5, naturals())); // [0, 1, 2, 3, 4]
console.log(take(5, naturals(100))); // [100, 101, 102, 103, 104]The take() above is fine when every call gets a fresh generator, as in take(5, naturals()). To keep pulling from the same generator, call next() yourself instead of looping with for...of. Calling next() never closes the generator, so the next call carries on where this one stopped:
function take(n, iterator) {
const result = [];
while (result.length < n) {
const { value, done } = iterator.next(); // pull values without closing it
if (done) break;
result.push(value);
}
return result;
}
const ids = naturals(1);
console.log(take(3, ids));
console.log(take(3, ids));
// [ 1, 2, 3 ]
// [ 4, 5, 6 ]Making your own iterable object
// Add [Symbol.iterator] to any object to make it iterable:
class NumberRange {
constructor(start, end) {
this.start = start;
this.end = end;
}
[Symbol.iterator]() {
let current = this.start;
const end = this.end;
return {
next() {
if (current <= end) {
return { value: current++, done: false };
}
return { value: undefined, done: true };
}
};
}
}
const nums = new NumberRange(1, 5);
console.log([...nums]); // [1, 2, 3, 4, 5]
for (const n of nums) {
console.log(n); // 1, 2, 3, 4, 5
}
// Can iterate multiple times — each call to [Symbol.iterator]() creates a fresh iterator:
console.log([...nums]); // [1, 2, 3, 4, 5] — works againOfficial Sources
- MDN Web Docs — Iteration protocols
- MDN Web Docs — function*
- ECMAScript 2024 — Generator Objects (§27.5)
- MDN Web Docs — for...of
- TC39 — Iterator Helpers Proposal
The Codex links only to official documentation and the ECMAScript specification. No third-party tutorials.