Learn by reading through in order

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
Each next() Moves the Pause Point Forward
numbers =callNumbers()Not a single lineof the body runs1stnext()Prints the textstops at yield 1012nd and 3rdnext()Stops at yield 102then yield 1034thnext()value: undefineddone: true
“Counter is open” is printed on the first next(), which then pauses at yield 101. Each next() runs to the next yield.

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.

Issue as many numbered tickets as you've prepared. ticketCount is already declared.

① Make issueTickets a generator function that yields 1 through count.

② After returning them all, return “Ticketing closed”.

③ Pass ticketCount and get a generator back.

④ Get the next value 4 times, printing value and done in the form “1 / false”.

(If it runs correctly, an explanation will appear.)

JavaScript / TypeScript Editor

Run code to see output

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();
}
What for...of Does for You
Loop withfor...ofCalls next()automaticallyChecks doneautomaticallyPuts value in stepand runs the bodyLoop yourselfwith whileresult =steps.next()Check !result.doneyourselfUse result.valuein the body
Both repeat columns 2–4 until done is true. With for...of, you don't write next() or the done check.

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...ofLoop variable on each iterationNotes
The array ["Received", "Shipped"]Each element ("Received" → "Shipped")Each loop creates a new iterator that starts from the first element
MapA 2-element [key, value] arrayEntries come out in the order they were added
GeneratorThe values written in yieldIt'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

Split product reviews into pages of 2 and print them. reviews is already declared.

① In paginate, yield each page as an array, in order from the start.

② Loop over the pages of reviews, 2 reviews per page.

③ For each page, print “Page 1: ” followed by the reviewer names joined with “, ”.

④ Finally, print the number of pages in the form “3 pages in total”.

JavaScript / TypeScript Editor

Run code to see output

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
The Second Spread Gives an Empty Array
1st[...slots]All 3 slots10:00 to 16:002nd[...slots]Empty arraylength is 0Called again[...pickupSlots()]Reads from startlength is 3
The second [...slots] starts reading from the end, where the first one left off. Only a newly called generator starts from the beginning.

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.

List the member IDs of approved membership applications. applications is already declared.

① Write a generator function that picks only the approved applications and returns their member IDs one at a time.

② Get a generator, take out only the first ID, and print it in the form “First: M-4102”.

③ Collect the rest into an array and print it in the form “2 remaining: M-4104, M-4105”.

④ Print all the IDs, including the first, joined with “, ”.

JavaScript / TypeScript Editor

Run code to see output

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)
How You Take Values Decides How Many Times next() Runs
serialFrom(1)endless numberingDestructured withconst [a, b] =for...of prints 3then breaks[...serialFrom(1)]to make an arraynext() iscalled twicenext() iscalled 3 timesdone neverbecomes trueCreates only1 and 2, then endsCreates 1 to 3and exits the loopLoops on next()and never ends
The left two columns stop because the number of values to take is fixed. Spread syntax reads until done is true, so you can't use it on an endless sequence.

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.

Work out retry wait times after a failed network request, doubling each time. retryDelays is already declared and yields values forever.

① Make take yield only the first count values from source, then finish.

② Starting from 100, put the first 4 values in an array and print them joined with “ → ”.

③ Take the first 5 values with a loop and print their sum in the form “Total: 3100ms”.

④ Pass 0 as the count, turn the result into an array, and print its length.

JavaScript / TypeScript Editor

Run code to see output
QUIZ

Knowledge Check

Answer each question one by one.

Q1Right after you call a function declared with function* and get a generator back, how far has the body run?

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?