061: Iterators, Iterables, and Generators
Outcomes
By the end of this lesson, you can:
- explain the iterable and iterator protocols;
- identify values that work with
for...of; - use
Symbol.iterator; - create custom iterables;
- write generator functions with
function*; - use
yieldto produce lazy sequences; - understand where generators improve clarity and where they add unnecessary complexity.
Iterable versus Iterator
An iterable provides a way to create an iterator. An iterator produces a sequence of { value, done } results.
Arrays, strings, Maps, and Sets are iterable.
for (const character of "JS") {
console.log(character);
}
Inspecting the Iterator
const values = [10, 20];
const iterator = values[Symbol.iterator]();
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());
Typical result:
{ value: 10, done: false }
{ value: 20, done: false }
{ value: undefined, done: true }
Custom Iterable
const range = {
start: 1,
end: 3,
[Symbol.iterator]() {
let current = this.start;
const end = this.end;
return {
next() {
if (current <= end) {
return {
value: current++,
done: false,
};
}
return {
value: undefined,
done: true,
};
},
};
},
};
console.log([...range]); // [1, 2, 3]
This is valid but verbose.
Generator Functions
Generators make iterator creation much easier.
function* range(start, end) {
for (let value = start; value <= end; value += 1) {
yield value;
}
}
console.log([...range(1, 3)]);
Calling a generator does not immediately run the entire body. It returns a generator object.
const sequence = range(1, 3);
console.log(sequence.next());
console.log(sequence.next());
Lazy Production
Generators are useful when values can be produced on demand.
function* ids(prefix = "ORD") {
let number = 1;
while (true) {
yield `${prefix}-${number}`;
number += 1;
}
}
const orderIds = ids();
console.log(orderIds.next().value);
console.log(orderIds.next().value);
console.log(orderIds.next().value);
The infinite loop is safe only because execution pauses at each yield and the consumer controls how far it advances.
yield*
Delegate to another iterable:
function* combined() {
yield* [1, 2];
yield* [3, 4];
}
console.log([...combined()]);
Passing Values Back into a Generator
Generators are two-way at a low level:
function* conversation() {
const name = yield "What is your name?";
yield `Hello ${name}`;
}
const flow = conversation();
console.log(flow.next().value);
console.log(flow.next("Maya").value);
This feature exists, but ordinary application workflows are often clearer with normal functions or async functions.
Iterables and Spread
Spread consumes iterables.
const letters = [..."JavaScript"];
So do many constructors:
const unique = new Set(["a", "b", "a"]);
const copied = [...unique];
Understanding iterables explains why these language features compose naturally.
Async Iteration Preview
An async iterable can produce values over time.
async function* pages(loadPage) {
let page = 1;
while (true) {
const result = await loadPage(page);
if (result.items.length === 0) {
return;
}
yield result.items;
page += 1;
}
}
Consumption:
for await (const items of pages(loadPage)) {
console.log(items);
}
Treat this as an advanced bridge to asynchronous streams rather than a required pattern for every API call.
Worked Example: Paginated Batch Generator
function* batches(items, size) {
if (!Number.isInteger(size) || size <= 0) {
throw new Error("size must be a positive integer");
}
for (let index = 0; index < items.length; index += size) {
yield items.slice(index, index + size);
}
}
for (const batch of batches([1, 2, 3, 4, 5], 2)) {
console.log(batch);
}
Output:
[1, 2] [3, 4] [5]
Advanced Notes: Iterator Cleanup and Async Iterables
A for...of loop can request cleanup from an iterator when iteration ends early. Generators support this through their return() behavior and finally.
function* values() {
try {
yield 1;
yield 2;
yield 3;
} finally {
console.log("cleanup");
}
}
for (const value of values()) {
console.log(value);
if (value === 2) {
break;
}
}
This matters when an iterator owns a resource or lifecycle.
Generator delegation
function* menu() {
yield "Home";
yield* ["Orders", "Reports"];
yield "Settings";
}
yield* delegates to any iterable, not just another generator.
Async iterable example
A paginated API can expose data one page at a time:
async function* fetchPages(fetchPage) {
let page = 1;
while (true) {
const result = await fetchPage(page);
if (result.items.length === 0) {
return;
}
yield result.items;
page += 1;
}
}
Consumer:
for await (const items of fetchPages(fetchPage)) {
render(items);
}
This can model streams naturally, but ordinary one-shot requests should stay ordinary. Advanced syntax is useful when it clarifies the domain, not when it merely demonstrates language knowledge.
Mistakes and Debugging
- expecting a generator call to execute the whole body immediately;
- forgetting that an iterator can be exhausted;
- reusing an exhausted generator instead of creating a new one;
- using generators when
map,filter, or a simple loop is clearer; - converting a huge lazy sequence to an array and losing the memory benefit.
Best Practices
- Prefer ordinary arrays and loops for ordinary collections.
- Use generators when lazy sequencing improves the problem model.
- Keep generator state simple.
- Use
for...ofinstead of manualnext()calls for normal consumption. - Learn the protocol because many JavaScript features build on it.
Exercises
Core
Manually call .next() on an array iterator.
Practice
Write function* countdown(from).
Professional Extension
Write function* chunks(array, size) and unit-test invalid sizes and the final partial chunk.
Recap
Iterables define how values can be consumed. Iterators perform the step-by-step consumption. Generators provide a concise language feature for building iterators and lazy sequences.
