Iterators and generators
The protocol behind for...of and spread, how a generator writes one for you, and why a paused function is the point.
Updated
The protocol first
for...of, spread, destructuring and Array.from all speak one protocol. An object is iterable if it has a [Symbol.iterator] method returning an iterator — an object with a next() that returns { value, done }.
That is the entire contract. Implement it and every syntax above starts working on your type:
const range = {
from: 1,
to: 3,
[Symbol.iterator]() {
let current = this.from;
const last = this.to;
return {
next: () => current <= last
? { value: current++, done: false }
: { value: undefined, done: true },
};
},
};
[...range]; // [1, 2, 3]
for (const n of range) { /* 1, 2, 3 */ }Arrays, strings, Map, Set, arguments and NodeLists are iterable. Plain objects are not — which is why for...of on an object throws, and why Object.entries exists.
A generator writes that for you
The same range, as a generator:
function* range(from, to) {
for (let i = from; i <= to; i++) yield i;
}
[...range(1, 3)]; // [1, 2, 3]Calling a generator function runs no code. It returns a generator object, which is both an iterator and iterable. Each next() runs until the next yield and then pauses, keeping the whole local state — variables, loop position, the call stack of that function — until asked again.
A function that can pause is the actual feature. Everything below follows from it.
Laziness, and sequences that do not fit in memory
function* naturals() {
let n = 0;
while (true) yield n++; // no, this does not hang
}
function* take(it, count) {
for (const value of it) {
if (count-- <= 0) return;
yield value;
}
}
[...take(naturals(), 5)]; // [0, 1, 2, 3, 4]The infinite loop is safe because nothing runs until something pulls. This is the difference between generators and array methods: .map().filter() builds a full intermediate array at every step; a generator pipeline produces one value at a time and never holds the whole sequence.
The same shape reads a large file, or pages an API, without loading it all:
async function* pages(url) {
let next = url;
while (next) {
const res = await fetch(next);
const data = await res.json();
yield* data.items; // yield* delegates to another iterable
next = data.nextUrl;
}
}
for await (const item of pages('/api/items')) { /* ... */ }for await...of and async function* are the async half of the same protocol, built on Symbol.asyncIterator.
yield is two-way
yield also *receives*. Whatever you pass to next(value) becomes the result of the paused yield expression:
function* conversation() {
const name = yield 'What is your name?';
return `Hello ${name}`;
}
const it = conversation();
it.next(); // { value: 'What is your name?', done: false }
it.next('Ada'); // { value: 'Hello Ada', done: true }The first next() has nowhere to deliver a value, so its argument is always discarded — a detail that catches people writing their first coroutine.
This two-way channel is what redux-saga is built on: the generator yields a description of an effect, the library performs it, and sends the result back in. The business logic stays synchronous-looking and, because it only yields plain objects, trivially testable.
What to reach for
| You want | Use |
|---|---|
your type to work with for...of | [Symbol.iterator] |
| a finite sequence you already have | an array |
| a sequence too large or infinite | a generator |
| a stream of awaited values | async function* + for await |
| to pause and resume logic | a generator, and only a generator |
Generators are stateful and single-use: once exhausted,
next()keeps returning{ done: true }. Iterate one twice and the second pass is empty — a bug that looks like missing data.
Now practice it
Reading this page is the cheap half. These are the exercises that make you use it.
- ChallengemediumPro
Iterable Range Generator
Implements both halves — the protocol by hand, then the generator that replaces it.
- Quizhard
Fundamentals · Advanced Patterns
Advanced language features, where iteration and laziness show up.
- Quizhard
Asynchronous Programming · Concurrency & Workers
Concurrency questions, including the async iteration this page ends on.