Iterators & Generators
Build lazy sequences and custom iteration protocols with iterators, generators, and async generators.
The Iterable Protocol
An object is iterable when it implements `[Symbol.iterator]()` returning an iterator. Iterators must provide `next()` returning `{ value, done }`. Built-in iterables include Array, String, Map, and Set.
`for...of` loops consume iterables. Spread and destructuring also use the iterator protocol under the hood, which is why you can spread Sets and Maps into arrays.
- `Symbol.iterator` makes objects iterable
- `next()` returns `{ value, done }`
- for...of, spread, destructuring use iterables
const range = {
*[Symbol.iterator]() {
for (let i = 0; i < 3; i++) yield i;
}
};
for (const n of range) console.log(n);Generator Functions
Generator functions (`function*`) return generator objects that are both iterators and iterables. `yield` pauses execution and produces a value; the generator resumes when `next()` is called again.
Generators enable lazy evaluation — infinite sequences, paginated API consumption, and tree traversal without building full arrays in memory. Each step computes the next value on demand.
- `function*` returns a generator
- `yield` pauses and produces values
- Lazy evaluation for large or infinite sequences
function* ids() {
let i = 0;
while (true) yield ++i;
}
const gen = ids();
gen.next().value; // 1Delegating and Passing Values
`yield*` delegates iteration to another iterable, flattening nested generator composition. `next(value)` sends values back into the generator, enabling two-way communication rarely needed in application code but used in coroutine-style libraries.
`return()` and `throw()` on iterators allow early termination and error injection. Generator cleanup runs in `finally` blocks inside the generator when iteration ends.
- `yield*` delegates to another iterable
- `next(value)` sends data into generator
- Use try/finally inside generators for cleanup
function* combined() {
yield* [1, 2];
yield* [3, 4];
}Async Generators and for await...of
Async generators (`async function*`) yield Promises and work with `for await...of` to consume async iterables page by page. Ideal for streaming API results, reading file chunks, and database cursors.
Backpressure matters in production streams — consume at the rate you can process and abort when consumers disconnect. Node.js readable streams implement async iteration in modern versions.
- `async function*` for async sequences
- `for await...of` consumes async iterables
- Handle backpressure and abort signals
async function* fetchPages(url) {
let page = 1;
while (true) {
const data = await fetch(`${url}?page=${page}`).then(r => r.json());
if (!data.length) return;
yield* data;
page++;
}
}Practical Use Cases
Use iterators when exposing collections from custom data structures without allocating intermediate arrays. Use generators for state machines where each yield represents a state transition readable in one function.
Most application code relies on built-in iterables. Reach for custom iterators when building libraries, parsing pipelines, or streaming large datasets — not for everyday array mapping where `.map()` is clearer.
- Custom collections benefit from iterators
- Generators for readable state machines
- Prefer array methods for simple transforms