In short
JavaScript iterators and generators let you control how values are produced one at a time. Learn the iteration protocol, Symbol.iterator, function* with yield, and for await...of. What is the iteration protocol? You have used for...of on arrays, strings, Maps and Sets.
JavaScript iterators and generators let you control how values are produced one at a time. Learn the iteration protocol, Symbol.iterator, function* with yield, and for await...of.
What is the iteration protocol?
You have used for...of on arrays, strings, Maps and Sets. But how does for...of know how to walk through them? They all follow the same simple rule, called the iteration protocol.
The rule is:
- An iterable is any object that has a method stored under the special key
Symbol.iterator. - That method returns an iterator: an object with a
next()method. - Each call to
next()returns an object like{ value: 'a', done: false }. When there is nothing left, it returns{ value: undefined, done: true }.
Think of a ticket dispenser at a bakery. Every time you press the button (next()), you get the next number. When the roll runs out, it tells you "done".
Let's see it by hand with an array.
const colors = ['red', 'green'];
const iterator = colors[Symbol.iterator](); // ask the array for its iterator
console.log(iterator.next()); // Output: { value: 'red', done: false }
console.log(iterator.next()); // Output: { value: 'green', done: false }
console.log(iterator.next()); // Output: { value: undefined, done: true }
for...of, the spread operator [...x], and destructuring all do exactly this behind the scenes. They keep calling next() until done is true.
How to make your own object iterable with Symbol.iterator
You can make your own objects work with for...of. Here is a countdown object that counts from a start number down to 1.
const countdown = {
from: 3,
[Symbol.iterator]() {
let current = this.from;
return {
next() {
if (current > 0) {
return { value: current--, done: false };
}
return { value: undefined, done: true };
},
};
},
};
for (const n of countdown) {
console.log(n);
}
// Output: 3, 2, 1
console.log([...countdown]); // Output: [3, 2, 1]
The [Symbol.iterator]() method returns an object with next(). Each time, next() gives back the current number and lowers it by one. When current reaches 0, it says done: true and the loop stops.
This works, but it is a lot of code for a simple idea. Generators make it much shorter.
Generator functions with function* and yield
A generator function is written with function* (note the star). Inside it, the yield keyword hands out a value and then pauses the function. The next time someone asks for a value, the function continues from where it stopped.
It is like reading a book with a bookmark: you read one page, put the bookmark in, and later you open the book right at that spot.
function* countdown(from) {
for (let i = from; i > 0; i--) {
yield i; // give out i, then pause here
}
console.log('Lift off!');
}
const gen = countdown(3);
console.log(gen.next()); // Output: { value: 3, done: false }
console.log(gen.next()); // Output: { value: 2, done: false }
for (const n of countdown(2)) {
console.log(n);
}
// Output: 2, 1, Lift off!
Calling countdown(3) does not run the code yet. It returns a generator object, which is both an iterator and an iterable. The code only runs a bit at a time, each time next() is called.
You can also use a generator as the Symbol.iterator method of a class or object: write *[Symbol.iterator]() { ... } and yield the values.
Tip: A generator is the easiest way to make something iterable. If you ever write a
next()method by hand, ask yourself iffunction*would be simpler.
Lazy sequences: values only when you need them
Because a generator pauses, it can describe a sequence that never ends, and it only computes the values you actually ask for. This is called being lazy.
// An endless sequence of ID numbers
function* idMaker() {
let id = 1;
while (true) {
yield `user-${id++}`;
}
}
// Take only the first few values from any iterable
function* take(iterable, count) {
if (count <= 0) return;
for (const item of iterable) {
yield item;
if (--count === 0) return; // stop early
}
}
const ids = idMaker();
console.log(ids.next().value); // Output: user-1
console.log(ids.next().value); // Output: user-2
console.log([...take(idMaker(), 3)]);
// Output: ["user-1", "user-2", "user-3"]
The while (true) loop looks scary, but it is safe. The generator pauses at every yield, so it only makes a new ID when asked. The take() helper stops after count items, so the spread finishes.
Common mistake: Never write
[...idMaker()]on an endless generator. The spread tries to read every value, which never ends, and your page freezes.
Note: Modern browsers (since 2025) also have built-in iterator helpers such as
idMaker().take(3).toArray()and.map()and.filter()on iterators. The hand-writtentake()above works everywhere.
Async iteration with for await...of
Sometimes each value arrives later, for example pages of results from a server. An async generator (async function*) can await inside, and you read it with for await...of.
async function* fetchPosts() {
for (let page = 1; page <= 2; page++) {
const response = await fetch(`https://jsonplaceholder.typicode.com/posts?_page=${page}&_limit=2`);
const posts = await response.json();
yield posts; // hand out one page at a time
}
}
async function showPosts() {
for await (const page of fetchPosts()) {
console.log(page.map((post) => post.title));
}
}
showPosts();
// Output: two arrays, each with 2 post titles
Each loop waits until the next page has loaded, then runs the body. This keeps the code reading top to bottom, just like a normal loop. You can find more details in MDN's guide to iterators and generators.
Practice exercise
Write a generator function range(start, end, step) that yields numbers from start up to (but not including) end, going up by step. Test it with [...range(0, 10, 2)], which should give [0, 2, 4, 6, 8]. Then make step default to 1.
Hint: use a normal for loop inside function* and yield each number.
Key takeaways
- The iteration protocol is simple:
Symbol.iteratorreturns an object whosenext()gives{ value, done }. for...of, spread and destructuring all use this protocol behind the scenes.- A
function*generator pauses at eachyield, which makes custom iterables short and readable. - Generators are lazy, so they can describe endless sequences safely if you only take what you need.
async function*andfor await...oflet you loop over values that arrive over time.