Generators & Iterators Get Certified
Generators and iterators give you fine-grained control over how sequences of values are produced and consumed. Instead of building an entire array in memory, you can produce values one at a time, pause execution, and even send values back into a running function.
What Is an Iterator?
An iterator is any object that implements a next()
method. Each call to next() returns an object with
two properties: value (the next item) and
done (a boolean that becomes true
when there are no more items).
What this code does: Manually creates a simple iterator object that yields three numbers and then signals it is finished.
const numbers = {
data: [10, 20, 30],
index: 0,
next() {
if (this.index < this.data.length) {
return { value: this.data[this.index++], done: false };
}
return { value: undefined, done: true };
}
};
console.log(numbers.next());
console.log(numbers.next());
console.log(numbers.next());
console.log(numbers.next());
const numbers = {data: [10, 20, 30]index: 0next() { — every iterator must expose next() returning { value, done }.if (this.index < this.data.length) { return { value: this.data[this.index++], done: false }; }return { value: undefined, done: true }} }numbers.next() four times — shows the iterator protocol by hand before generators hide this boilerplate.Making an Object Iterable with Symbol.iterator
An object becomes iterable when it has a method keyed
by Symbol.iterator. That method must return an
iterator. Once an object is iterable, you can use it with
for...of, the spread operator, and more.
What this code does: Adds
Symbol.iterator to a custom object so it works with
for...of and the spread operator.
const range = {
from: 1,
to: 4,
[Symbol.iterator]() {
let current = this.from;
const last = this.to;
return {
next() {
if (current <= last) {
return { value: current++, done: false };
}
return { done: true };
}
};
}
};
for (const num of range) {
console.log(num);
}
console.log([...range]);
const range = { from: 1, to: 4,[Symbol.iterator]() { — implementing this symbol lets for...of and spread work.let current = this.from; const last = this.to;return {next() { if (current <= last) return { value: current++, done: false }; return { done: true }; }} } }for (const num of range) { console.log(num); }console.log([...range]) — both consume the iterator produced by Symbol.iterator.Generator Functions (function*)
A generator function is written with function*.
Inside it, the yield keyword pauses execution and
returns each value automatically.
What this code does: A generator function yields
a countdown from a starting number down to zero. Each call to
next() advances one step.
function* countdownGenerator(start) {
for (let i = start; i >= 0; i--) {
yield i;
}
}
const countdown = countdownGenerator(3);
console.log(countdown.next());
console.log(countdown.next());
console.log(countdown.next());
console.log(countdown.next());
console.log(countdown.next());
function* countdownGenerator(start) — the * makes this a generator; calling it returns an iterator, it does not run the body yet.for (let i = start; i >= 0; i--) { yield i; } — each yield returns a value and suspends until the next next() call.}const countdown = countdownGenerator(3)countdown.next() five times — values 3,2,1,0 then { done: true }. Generators implement the iterator protocol automatically.
Calling a generator function doesn’t run its body immediately —
it returns a special generator object. Each call to
next() runs the function until it hits a
yield, pauses there, and hands back that value.
Generators Work Directly With for...of
Because a generator object is automatically iterable, you can use
it directly in a for...of loop without any extra
setup.
What this code does: A generator yields weekday
names one by one. Because generators are iterable, you can loop
over them directly with for...of.
function* weekdays() {
yield "Monday";
yield "Tuesday";
yield "Wednesday";
yield "Thursday";
yield "Friday";
}
for (const day of weekdays()) {
console.log(day);
}
function* weekdays()yield "Monday"; ... yield "Friday"; — each pause produces one day.}for (const day of weekdays()) { console.log(day); } — for...of keeps calling next() until done is true. A new generator instance is created each time you call weekdays().You can also spread a generator directly into an array.
What this code does: Spreads the values produced by a generator into a real array using the spread operator.
function* weekdays() {
yield "Monday";
yield "Tuesday";
yield "Wednesday";
}
const days = [...weekdays()];
console.log(days);
function* weekdays() { yield "Monday"; yield "Tuesday"; yield "Wednesday"; }const days = [...weekdays()] — spread also consumes any iterable; the generator runs until done and the yielded values become array elements.console.log(days)The done Value and return Inside a Generator
A return statement inside a generator ends it, and
that returned value comes back with done: true. It
is not included by a for...of loop, which
stops as soon as it sees done: true.
What this code does: Shows that a
return inside a generator finishes the sequence and
supplies a final value (with done: true) that
for...of does not include.
function* deliveryStatus() {
yield "Order placed";
yield "Shipped";
return "Delivered";
}
const status = deliveryStatus();
console.log(status.next());
console.log(status.next());
console.log(status.next());
function* deliveryStatus()yield "Order placed"; yield "Shipped";return "Delivered" — return ends the generator; the value appears in the last next() result with done: true.}const status = deliveryStatus()status.next() — first two are yields (done: false); the third carries the return value and done: true. for...of ignores the return value.Passing Values Into next()
A value passed to next() becomes the result of the
yield expression that was paused, allowing two-way
communication with a running generator.
What this code does: Demonstrates two-way
communication: values you pass to next() become the
result of the paused yield expression.
function* orderTaker() {
const firstItem = yield "What would you like to order?";
const secondItem = yield "Anything else, after " + firstItem + "?";
return "Order complete: " + firstItem + " and " + secondItem;
}
const order = orderTaker();
console.log(order.next());
console.log(order.next("coffee"));
console.log(order.next("a croissant"));
function* orderTaker()const firstItem = yield "What would you like to order?" — the value of the yield expression is whatever is passed to the *next* next() call.const secondItem = yield "Anything else, after " + firstItem + "?"return "Order complete: " + firstItem + " and " + secondItem}const order = orderTaker()order.next() — runs to the first yield; the argument to the very first next() is ignored.order.next("coffee") — becomes the value of firstItem.order.next("a croissant") — becomes secondItem; then the return value is delivered.
The first call to next() can’t pass in a useful
value yet, since the generator hasn’t reached its first
yield to receive it.
Infinite Generators
Since a generator only computes a value when next()
is actually called, it can represent a sequence that never ends —
something that would be impossible to store as a regular array.
What this code does: Creates an infinite generator that yields even numbers on demand. Values are computed only when requested.
function* evenNumbers() {
let current = 0;
while (true) {
yield current;
current += 2;
}
}
const evens = evenNumbers();
console.log(evens.next().value);
console.log(evens.next().value);
console.log(evens.next().value);
console.log(evens.next().value);
function* evenNumbers()let current = 0while (true) { — safe because values are only produced when next() is called.yield currentcurrent += 2} }const evens = evenNumbers()evens.next().value — 0, 2, 4, 6. Never put an infinite generator in a bare for...of without a break or limit.
This works safely because nothing is computed until it’s asked
for — looping over this generator with a plain
for...of without a break would run
forever, so infinite generators are usually paired with a limit.
Delegating to Another Generator With yield*
yield* delegates to another iterable, yielding each
of its values in turn, which is useful for composing smaller
generators into a larger one.
What this code does: Uses yield*
to compose smaller generators into one larger sequence that
yields every dish in order.
function* appetizers() {
yield "Bruschetta";
yield "Calamari";
}
function* mainCourses() {
yield "Grilled Salmon";
yield "Mushroom Risotto";
}
function* fullMenu() {
yield* appetizers();
yield* mainCourses();
yield "Tiramisu";
}
for (const dish of fullMenu()) {
console.log(dish);
}
function* appetizers() { yield "Bruschetta"; yield "Calamari"; }function* mainCourses() { yield "Grilled Salmon"; yield "Mushroom Risotto"; }function* fullMenu() {yield* appetizers() — yield* iterates the other generator and yields each of its values in turn.yield* mainCourses()yield "Tiramisu"}for (const dish of fullMenu()) { console.log(dish); } — yields all appetizers, then mains, then dessert.Practical Example: A Custom Range Generator
Let’s build a reusable generator that produces a numeric range
with a custom step, similar to Python’s range().
What this code does: A practical range generator
(like Python’s range) that yields numbers from
start up to (but not including) end with a custom step.
function* range(start, end, step = 1) {
for (let value = start; value < end; value += step) {
yield value;
}
}
for (const value of range(0, 10, 2)) {
console.log(value);
}
console.log([...range(5, 20, 5)]);
function* range(start, end, step = 1) — default step of 1, like Python’s range.for (let value = start; value < end; value += step) { yield value; } — exclusive end.}for (const value of range(0, 10, 2)) { console.log(value); } — 0,2,4,6,8.console.log([...range(5, 20, 5)]) — [5, 10, 15]. Create a new generator each time you need a fresh sequence.When to Reach for Generators
| Situation | Why a Generator Helps |
|---|---|
| Large or infinite sequences | Values are produced one at a time, not stored all at once |
| Custom iteration logic | Makes an object usable with for...of and spread |
| Step-by-step processes | yield naturally pauses and resumes multi-step logic |
| Composable sequences | yield* combines smaller generators into larger ones |
Common Mistakes With Generators and Iterators
- Forgetting the asterisk (
*) afterfunction, which silently turns it into a regular function whereyieldisn’t valid. - Looping over an infinite generator with a plain
for...ofand nobreak, causing the program to hang. - Expecting the value passed to the very first
next()call to be used, when the generator hasn’t reached ayieldto receive it yet. - Assuming a generator’s
returnvalue shows up in afor...ofloop, when it is actually excluded oncedonebecomestrue. - Reusing an already-finished generator object, expecting it to start over — generators can only be iterated through once.
Best Practices for Generators and Iterators
- Prefer generator functions over manually written iterator objects whenever possible — they’re far less error-prone.
- Use
yield*to compose generators instead of duplicating logic across them. - Always pair an infinite generator with a limiting condition, such as a counter or a
breakinside the loop consuming it. - Implement
Symbol.iteratoron custom objects when you want them to work naturally withfor...ofand the spread operator. - Create a new generator instance each time you need to iterate from the beginning, rather than reusing a finished one.
Quick Summary
- An iterator is an object with a
next()method returning{ value, done }. - An object becomes iterable by implementing
Symbol.iterator, enablingfor...ofand the spread operator. - Generator functions (
function*) automatically build an iterator usingyieldto pause and resume. yield*delegates to another iterable, combining sequences together.- Generators can represent infinite sequences safely, since values are only computed on demand.
- Values can flow both out (via
yield) and in (via arguments tonext()) during a generator’s execution.
Try It Yourself
Modify the generator below. Try changing the starting values, or
adding a limit so it doesn’t run forever when consumed with a
for...of loop.
What this code does: A practice Fibonacci
generator that yields the first limit numbers of
the sequence. Experiment by changing the limit or the starting
values.
function* fibonacci(limit) {
let previous = 0;
let current = 1;
for (let i = 0; i < limit; i++) {
yield previous;
[previous, current] = [current, previous + current];
}
}
console.log([...fibonacci(8)]);
function* fibonacci(limit)let previous = 0; let current = 1;for (let i = 0; i < limit; i++) { — a finite limit prevents an infinite sequence.yield previous[previous, current] = [current, previous + current] — destructuring swap/update in one step.} }console.log([...fibonacci(8)]) — first 8 Fibonacci numbers. Change limit or starting values to experiment.Tip: If you’re about to write an object with aSymbol.iteratormethod that manually tracks an index and returns{ value, done }by hand, try rewriting it as a generator function first — it’s almost always shorter and easier to follow.
Understanding generators and iterators is useful before moving on to asynchronous iteration, Proxies, Reflect, and more advanced JavaScript concepts.
🧪 Test Your JavaScript Code
Edit the code on the left and click "Run Code" to see the console output on the right.
Click "Run Code" to see the output here.
Code Explanation
The complete example stays visible. Use Previous and Next to move through its logical blocks.
Select Next to begin exploring the code.