Iterators and Generators
Learn how iterators and generators work in JavaScript and how they power the for...of loop.
Iterators and Generators
You have used for...of to loop over arrays, strings, Maps, and Sets. But have you ever wondered what makes something iterable? Why can you for...of over an array but not a plain object?
The answer is iterators — a protocol that defines how JavaScript steps through values one at a time. And generators are a powerful way to create iterators without all the boilerplate.
The Iterator Protocol
An iterator is any object that has a next() method. Each call to next() returns an object with two properties:
value— the current valuedone—truewhen there are no more values,falseotherwise
const iterator = {
values: [1, 2, 3],
index: 0,
next() {
if (this.index < this.values.length) {
return { value: this.values[this.index++], done: false };
}
return { value: undefined, done: true };
}
};
console.log(iterator.next()); // { value: 1, done: false }
console.log(iterator.next()); // { value: 2, done: false }
console.log(iterator.next()); // { value: 3, done: false }
console.log(iterator.next()); // { value: undefined, done: true }Every call to next() advances the iterator one step. When done is true — nothing more to iterate.
The Iterable Protocol
An iterable is an object that has a Symbol.iterator method — a special method that returns an iterator.
This is exactly what makes arrays, strings, Maps, and Sets work with for...of. They all implement Symbol.iterator.
const arr = [1, 2, 3];
// Get the iterator from the array
const iterator = arr[Symbol.iterator]();
console.log(iterator.next()); // { value: 1, done: false }
console.log(iterator.next()); // { value: 2, done: false }
console.log(iterator.next()); // { value: 3, done: false }
console.log(iterator.next()); // { value: undefined, done: true }When you use for...of, JavaScript calls Symbol.iterator on the object to get an iterator, then repeatedly calls next() until done is true.
Making a Custom Iterable
You can make any object iterable by adding Symbol.iterator to it.
const range = {
from: 1,
to: 5,
[Symbol.iterator]() {
let current = this.from;
const last = this.to;
return {
next() {
if (current <= last) {
return { value: current++, done: false };
}
return { value: undefined, done: true };
}
};
}
};
for (const num of range) {
console.log(num);
}
// 1
// 2
// 3
// 4
// 5
// Also works with spread
console.log([...range]); // [1, 2, 3, 4, 5]
// And destructuring
const [first, second, ...rest] = range;
console.log(first, second, rest); // 1 2 [3, 4, 5]Anything that uses iterables under the hood — for...of, spread, destructuring, Array.from() — now works on your custom object.
Generators — The Easy Way to Create Iterators
Writing iterators manually is verbose. Generators are a special kind of function that creates iterators automatically — with much cleaner syntax.
A generator function uses function* (note the asterisk) and yield to produce values one at a time.
function* simpleGenerator() {
yield 1;
yield 2;
yield 3;
}
const gen = simpleGenerator();
console.log(gen.next()); // { value: 1, done: false }
console.log(gen.next()); // { value: 2, done: false }
console.log(gen.next()); // { value: 3, done: false }
console.log(gen.next()); // { value: undefined, done: true }How Generators Work
The generator function does not run when called — it returns a generator object. Code only runs when you call .next(). It runs until it hits a yield — pauses there — returns the yielded value — and waits for the next .next() call.
function* steps() {
console.log("Step 1 running");
yield "Step 1 done";
console.log("Step 2 running");
yield "Step 2 done";
console.log("Step 3 running");
yield "Step 3 done";
console.log("All steps complete");
}
const gen = steps();
console.log(gen.next().value);
// Step 1 running
// Step 1 done
console.log(gen.next().value);
// Step 2 running
// Step 2 done
console.log(gen.next().value);
// Step 3 running
// Step 3 done
console.log(gen.next().value);
// All steps complete
// undefinedThe function literally pauses at each yield and resumes from exactly that point on the next .next() call. No other function in JavaScript can do this.
Using Generators With for...of
Generator objects are iterables — you can use them directly with for...of.
function* range(from, to, step = 1) {
for (let i = from; i <= to; i += step) {
yield i;
}
}
for (const num of range(1, 10, 2)) {
console.log(num);
}
// 1
// 3
// 5
// 7
// 9
console.log([...range(1, 5)]); // [1, 2, 3, 4, 5]This replaces the verbose custom iterable we wrote earlier with three clean lines.
Infinite Generators
Generators can yield values forever — they only run when you ask for the next value, so they never block.
function* infiniteCounter(start = 0) {
let count = start;
while (true) {
yield count++;
}
}
const counter = infiniteCounter(1);
console.log(counter.next().value); // 1
console.log(counter.next().value); // 2
console.log(counter.next().value); // 3
// ... forever, but only when you ask// Take only what you need
function take(generator, n) {
const result = [];
for (const value of generator) {
result.push(value);
if (result.length === n) break;
}
return result;
}
console.log(take(infiniteCounter(1), 5)); // [1, 2, 3, 4, 5]A regular array of infinite numbers is impossible — you would run out of memory. A generator produces values lazily — only when requested.
yield* — Delegating to Another Iterable
yield* delegates to another iterable — yielding all its values in sequence.
function* numbers() {
yield 1;
yield 2;
}
function* letters() {
yield "a";
yield "b";
}
function* combined() {
yield* numbers(); // yield all values from numbers
yield* letters(); // then all from letters
yield "done";
}
console.log([...combined()]); // [1, 2, "a", "b", "done"]// Works with any iterable
function* flatten(arr) {
for (const item of arr) {
if (Array.isArray(item)) {
yield* flatten(item); // recursively flatten
} else {
yield item;
}
}
}
const nested = [1, [2, 3], [4, [5, 6]]];
console.log([...flatten(nested)]); // [1, 2, 3, 4, 5, 6]Passing Values Into a Generator
next() can accept a value — which becomes the result of the yield expression inside the generator.
function* calculator() {
const a = yield "Enter first number:";
const b = yield "Enter second number:";
yield `Result: ${a + b}`;
}
const calc = calculator();
console.log(calc.next().value); // "Enter first number:"
console.log(calc.next(10).value); // "Enter second number:" — 10 is assigned to a
console.log(calc.next(20).value); // "Result: 30" — 20 is assigned to bThe first .next() starts the generator — the value passed to the first .next() is ignored. Each subsequent .next(value) resumes the generator and that value becomes what yield evaluates to.
Real Use — Paginated API
Generators are perfect for lazy data loading — fetch the next page only when needed.
async function* fetchPages(baseUrl) {
let page = 1;
while (true) {
const response = await fetch(`${baseUrl}?page=${page}`);
if (!response.ok) break;
const data = await response.json();
if (data.length === 0) break; // no more pages
yield data; // yield this page's data
page++;
}
}
// Usage — process one page at a time
async function loadAllUsers() {
const pages = fetchPages("https://api.example.com/users");
for await (const page of pages) {
for (const user of page) {
console.log(user.name);
}
}
}async function* creates an async generator — it can await inside and yield data. for await...of consumes it one page at a time. You only fetch the next page when you need it — never loading everything at once.
for await...of works with async iterables — objects that return Promises from next(). Async generators are the cleanest way to create them.
A Real Example — ID Generator
function* createIdGenerator(prefix = "id") {
let id = 1;
while (true) {
yield `${prefix}-${String(id++).padStart(4, "0")}`;
}
}
const userIds = createIdGenerator("user");
const orderIds = createIdGenerator("order");
console.log(userIds.next().value); // user-0001
console.log(userIds.next().value); // user-0002
console.log(userIds.next().value); // user-0003
console.log(orderIds.next().value); // order-0001
console.log(orderIds.next().value); // order-0002Each generator maintains its own state independently. userIds and orderIds never interfere with each other. Clean, stateful, infinite ID generation.
Summary
- An iterator is an object with a
next()method that returns{ value, done } - An iterable is an object with a
Symbol.iteratormethod that returns an iterator for...of, spread, and destructuring all use the iterator protocol under the hood- A generator function uses
function*andyieldto produce values lazily - Generators pause at each
yieldand resume on the next.next()call - Generators are iterables — use them with
for...of, spread, and destructuring - Infinite generators produce values forever without blocking — only run when asked
yield*delegates to another iterable — yielding all its values- Pass values into a generator through
next(value)— becomes the result ofyield - Async generators combine
async function*withyield— consumed withfor await...of