Iterators & Generators
Symbol.iterator, generators, async iterators, iterator helpers.
Anything you can for...ofis "iterable": arrays, strings, Maps, Sets, NodeLists. The thing that ties them together is a protocol you can implement yourself, and a special function type called a generator that makes implementing it trivial. ES2025 also added iterator helpers, which finally bring the array methods to lazy sequences.
The iterator protocol
An object is iterable if it has a Symbol.iterator method that returns an iterator. An iterator has a .next() method that returns { value, done }. Here's the minimal hand version:
const range = {
from: 1,
to: 3,
[Symbol.iterator]() {
let current = this.from;
const last = this.to;
return {
next() {
return current <= last
? { value: current++, done: false }
: { value: undefined, done: true };
},
};
},
};
for (const n of range) console.log(n); // 1, 2, 3
[...range]; // [1, 2, 3]That's the long way. Generators give you the same thing with a fraction of the code.
Generators: function* and yield
A generator function is declared with function* and uses yield to produce values. Calling it returns a generator (which is both iterator and iterable):
function* range(from, to) {
for (let i = from; i <= to; i++) {
yield i;
}
}
for (const n of range(1, 3)) console.log(n); // 1, 2, 3
[...range(1, 5)]; // [1, 2, 3, 4, 5]yield pauses the function and hands the value out to the caller. The next .next() call resumes execution right after that yield. It's a coroutine, basically.Lazy = only compute what's needed
Because generators yield one value at a time, you can describe infinite sequences and only pay for what you consume:
function* naturals() {
let n = 1;
while (true) yield n++;
}
const it = naturals();
it.next().value; // 1
it.next().value; // 2
it.next().value; // 3
// Don't try [...naturals()] - that would never finish.Iterator helpers (ES2025)
For years, arrays had .map, .filter, .reduce, and iterators didn't. ES2025 fixed that. You can chain methods directly on an iterator, and they stay lazy:
function* naturals() {
let n = 1;
while (true) yield n++;
}
// All lazy: nothing runs until .toArray() pulls
const result = naturals()
.filter(n => n % 2 === 0)
.map(n => n * n)
.take(5)
.toArray();
// [4, 16, 36, 64, 100]The new helpers include:
.map(fn),.filter(fn),.flatMap(fn).take(n)- first n values,.drop(n)- skip n values.find,.some,.every,.reduce.toArray()to materialize
.find or .take(5) on an infinite generator only pulls as many values as needed.Async iterators: for-await-of
Iterators have an async cousin: Symbol.asyncIterator and for await...of. Made for streams of promises, like paged API responses or file reads:
async function* pagedUsers() {
let page = 1;
while (true) {
const res = await fetch("/api/users?page=" + page);
const data = await res.json();
if (data.items.length === 0) return;
for (const user of data.items) yield user;
page++;
}
}
for await (const user of pagedUsers()) {
console.log(user.name); // one at a time, across pages
}Try it: a lazy range
Quiz
What does `yield` do inside a generator function?
Recap
- Iterable + iterator is a protocol:
Symbol.iteratorreturns an object withnext(). function*+yieldis the easy way to implement it.- Generators are lazy. They can model infinite sequences without blowing memory.
- ES2025 iterator helpers (
.map,.filter,.take,.toArray) let you compose lazy pipelines. - Async iterators +
for await...ofhandle streams of promises (e.g. paged API responses).