Q1Right after you call a function declared with function* and get a generator back, how far has the body run?
Iterators and Generators — Getting Values One at a Time
Iterators hand out values one at a time, and generators pause at each yield. Covers next(), for...of and spread, and taking just what you need from an endless sequence.
When you hand out numbered tickets one by one, or show a long list of reviews a few at a time, building every number or page up front means preparing ones that never get used. You can write a function that remembers the next value with a closure, but then you have to track how far you've gone and whether you're done yourself.
This article covers iterators, which return values one at a time, and generators, which let you write them as functions.
Functions That Resume Where They Left Off — Generators and yield
Say you want a function that hands out the next number at a service counter — 101, then 102, then 103 — each time you call it. A regular function ends as soon as it runs return, and the next call starts over from the top, so you'd need an outer variable to remember how far you've gotten.
When you call a generator function (a function declared with function* that can use yield inside), the body doesn't run; instead you get back a generator (an object that advances the body a little at a time). Each call to its next() runs the body up to the next yield and pauses there. next() returns an object whose value holds the yielded value and whose done tells you whether the body has finished.
// Declaring with function* makes a generator function
function* callNumbers() {
console.log("Counter is open");
yield 101;
yield 102;
yield 103;
}
// Calling it doesn't run the body; it just returns a generator
const numbers = callNumbers();
// Each next() call runs to the next yield and pauses
const first = numbers.next(); // "Counter is open" is printed here
console.log(`${first.value} / ${first.done}`); // 101 / false
console.log(numbers.next().value); // 102
console.log(numbers.next().value); // 103
// Once there's no yield left, done becomes true
const last = numbers.next();
console.log(`${last.value} / ${last.done}`); // undefined / true
Once the body reaches its end or runs return, done becomes true, and later next() calls don't run the body anymore. The generator itself keeps track of how far it has gone, so you don't need an outer variable to remember the counter number.
You Can't Write yield Inside a Callback
You can only write yield inside the braces of a function declared with function*. An arrow function passed to forEach is a separate function, so writing it there is a SyntaxError — and the error message doesn't mention yield. To yield an array's elements one at a time, write yield in the body of a for or for...of loop.
Looping Without Writing next() — The Iterator Protocol
Say you want to show a delivery's status in order: “Received”, “Shipped”, “Out for delivery”. If you write one next() call per step, you need a line for every step and have to add more whenever a step is added. Looping with while still means writing the done check and the next next() call yourself.
The rule that each next() call returns an object with value and done is called the iterator protocol, and an object that follows it is an iterator. Generators from the previous section are iterators. for...of keeps calling next() until done is true, putting each value into the loop variable.
function* deliverySteps() {
yield "Received";
yield "Shipped";
yield "Out for delivery";
}
// for...of calls next() until done is true
for (const step of deliverySteps()) {
console.log(step); // 3 lines: Received / Shipped / Out for delivery
}
// The same behavior written with while and next()
const steps = deliverySteps();
let result = steps.next();
while (!result.done) {
console.log(result.value); // 3 lines: Received / Shipped / Out for delivery
result = steps.next();
}
You can pass for...of any iterable value (one that for...of can get an iterator from), such as an array, a Map, or a generator. for...of doesn't care what kind of value it is; it just calls next() on the iterator it gets, so you loop over all of them the same way. The table below shows what goes into the loop variable.
| What you pass to for...of | Loop variable on each iteration | Notes |
|---|---|---|
| The array ["Received", "Shipped"] | Each element ("Received" → "Shipped") | Each loop creates a new iterator that starts from the first element |
| Map | A 2-element [key, value] array | Entries come out in the order they were added |
| Generator | The values written in yield | It's its own iterator, so any progress made with next() carries over |
| An object like { name: "Pen" } | Nothing (TypeError: … is not iterable) | It isn't iterable, so use for...in or Object.entries to go through its keys |
Turning It into an Array — Spread Syntax and One-Time Use
Say you want to show pickup time slots on one line, like “10:00 / 13:00 / 16:00”. A generator isn't an array, so it has no join or length, and on its own you can't even count its values. Pushing each value into an array with for...of adds extra lines just for that.
If you put a generator inside array brackets with the spread syntax ..., it pulls out values until done is true and puts them in order in a new array. Once you write const slotList = [...slots];, you can use both join and length.
function* pickupSlots() {
yield "10:00";
yield "13:00";
yield "16:00";
}
// A generator isn't an array, so it has no length
const slots = pickupSlots();
console.log(slots.length); // undefined
// Spread syntax inside [] puts the values in order
const slotList = [...slots];
console.log(slotList.join(" / ")); // 10:00 / 13:00 / 16:00
// Spreading a used-up generator again gives an empty array
console.log([...slots].length); // 0
// Calling the function again gives a fresh generator that starts from the beginning
console.log([...pickupSlots()].length); // 3
When you need the same values in two or more places, spread them into an array once, store it in a variable, and reuse that array. A generator you've already looped through to the end with for...of also gives an empty array if you spread it afterward.
Making Only What You Need — Lazy Evaluation and Endless Sequences
When you give notices serial numbers starting from 1, you don't know ahead of time how many there will be. If you build 1000 numbers in an array up front, the rest get computed even when you only use 3. And with no upper limit, you could never finish building the array.
Generators give you lazy evaluation (computing a value only at the moment it's needed). Even a function that yields endlessly with while (true) only runs its body as far as next() has been called. Array destructuring also works on generators, taking out exactly as many values as you list on the left side.
function* serialFrom(start) {
let number = start;
while (true) {
console.log(`Created ${number}`);
yield number;
number++;
}
}
// Destructuring takes out as many values as you list on the left
const [first, second] = serialFrom(1); // Created 1 / Created 2
console.log(`${first}, ${second}`); // 1, 2
// for...of stops taking values once you break out
for (const number of serialFrom(1)) {
console.log(`Notice No.${number}`);
if (number === 3) break;
}
// Created 1 / Notice No.1 / Created 2 / Notice No.2
// Created 3 / Notice No.3 (4 is never created)
To turn an endless sequence into an array, put another generator in between that takes a set number of values and then returns. Once that in-between generator finishes, done becomes true, so spread syntax stops there too.
If It Never Reaches yield, next() Never Returns
If you write a while (true) loop that only yields when an if condition is met, and the condition is never met, next() never returns. Execution never finishes, and the lines below it never run. When you loop endlessly, first make sure a value that meets the condition is guaranteed to come eventually.
Knowledge Check
Answer each question one by one.
Q2If you run [...slots] twice in a row on a generator slots that yields 3 values, what's the length the second time?
Q3For a generator gen that yields endlessly with while (true), which of these never finishes?