Generators
Learn JavaScript generator functions: how function* and yield pause work, why generators are iterators, how yield* delegates, how return and throw finish or interrupt, and how infinite lazy sequences stay safe.
- 01Pause and go onSay exactly when a generator's code runs, and what each
yieldhands out. - 02Use generators as iteratorsLoop over a generator object with
for...ofand spread, and know it works only once. - 03Stop, interrupt, and combine generatorsUse
return(),throw(),yield*, and endless generators without surprises.
Functions that can pause
A normal function runs from start to end in one go. You call it, and it keeps running until it returns a value or throws an error. A generator function is different. It can stop in the middle, hand you a value, and wait. Later you tell it to go on, and it carries on from the exact place where it stopped.
The keyword that hands out a value and pauses is yield. The method that says "go on" is next(). While the function waits, it keeps all its local variables, so nothing is lost.
Generators build on the last lesson, the iteration protocols. There you wrote a next() method by hand, and it gave back objects like { value, done }. A generator makes that next() method for you. You write normal code with loops and yield, and JavaScript makes the results. The object you get also works with for...of, spread (...), and destructuring.
A generator function is written with function*: the word function with a star. Calling it does not run its code. It gives you a generator object. Each time you call the object's next() method, the code runs until the next yield, hands out that value, and pauses. The object also has return() and throw() methods, which you'll meet later in this lesson.
Think of a storyteller who stops at the end of every scene. When you say "go on", they don't start the story again. They remember the characters and the exact sentence where they stopped, and they carry on from there.
- In real life: The storyteller starts the tale
- In JavaScript: The first
next()starts the function's code - In real life: They stop at the end of a scene
- In JavaScript: Each
yieldhands out a value and pauses - In real life: They remember exactly where they stopped
- In JavaScript: The generator keeps its variables and its place
- In real life: You say "go on"
- In JavaScript: Calling
next()again carries on after thatyield
In this lesson you will:
- Step through a generator, one line at a time.
- Send commands to a live generator with buttons.
- Use
yield*to hand out the values of another array or generator. - Safely take a few values from a list that never ends.
function* and yield
STEP THROUGHTo write a generator, put a star after the word function, like this: function*. Inside it, use yield to hand out a value.
Here is the surprising part: calling a generator function runs none of its code. It only gives you a generator object. The code starts on the first next() call, and it runs until the first yield.
The three cards below show the three steps. They use the story example that you'll step through soon.
const tale = story();No line inside story has run yet.
tale.next()The code runs until the next yield, a return, or an error.
{ value, done: false }The function waits here. Its variables are kept.
function* makeTea() { console.log("body ran"); yield "tea";} const cup = makeTea();console.log("created");Lines 1 to 4 write a generator called makeTea. Line 2 would print body ran. Line 6 calls makeTea() and keeps the generator object in cup. But line 2 does not run, because nobody called next(). Line 7 prints created, and that is the only output.
This is useful when the work inside should wait until someone really asks for a value.
function* colors() { yield "red"; yield "blue";} const list = colors();console.log(JSON.stringify(list.next()));console.log(JSON.stringify(list.next()));console.log(JSON.stringify(list.next()));Lines 7, 8, and 9 call list.next() three times. JSON.stringify turns each result into text, so you can see its two properties:
valueis the value handed out byyield.doneisfalsewhile the generator is paused at ayield, andtrueonce its code has finished.
The first call prints {"value":"red","done":false}. The second prints {"value":"blue","done":false}. The third call finds no more yield, so it prints {"done":true}. Its value is undefined, and JSON.stringify leaves out properties that are undefined.
next() runs the code up to the next yield and pauses there. The third call finds no more yield, so the result says done: true.When you call next() yourself, check done to know when to stop.
Next, step through a longer example called story. Each step shows one line running. The steps were recorded from real JavaScript, but this is not a view inside the JavaScript engine. The "Running in" row shows where the code is right now: in the main script, or inside the generator.
You can also change the last command, on line 20, to see three endings: go on as normal, finish early, or throw an error into the generator.
Guess first: what runs when story() is called? Step through to see that none of its code runs until next() starts it.
script
function* story() { console.log("story starts"); try { yield "chapter 1"; console.log("after first pause"); yield "chapter 2"; console.log("after second pause"); return "the end"; } catch (error) { console.log("caught: " + error.message); return "recovered"; } finally { console.log("cleanup"); }} console.log("created");tale.next();tale.next();Two more details are worth knowing.
First, next() can take a value, like next("hi"). That value becomes the result of the paused yield. But on the first call, no yield has paused yet, so the value has nowhere to go. JavaScript ignores it. Sending values into a generator gets its own lesson later: Generators for control flow.
Second, a generator's local variables are not shared with the rest of the program. They belong to that one paused generator object. It's like the local variables of a normal function call, which you saw in the Execution contexts lesson.
function* reply() { const answer = yield "ready"; console.log("answer: " + answer);} const task = reply();console.log(JSON.stringify(task.next("ignored")));console.log(JSON.stringify(task.next("accepted")));Line 2 says const answer = yield "ready". The yield hands out ready and pauses. When the generator goes on, the value sent with next() is stored in answer.
Line 7 calls next("ignored"). The code runs to the yield, and line 7 prints {"value":"ready","done":false}. The word ignored is dropped, because no yield was paused yet.
Line 8 calls next("accepted"). Now the paused yield receives accepted, so line 3 prints answer: accepted. Then the code ends, and line 8 prints {"done":true}.
Generators as iterators
INTERACTIVEA generator object is both an iterator and an iterable. You met both words in the last lesson:
- An iterator has a
next()method that gives one result at a time. - An iterable has a
[Symbol.iterator]()method, sofor...ofand spread can loop over it.
A generator object has both methods. Its [Symbol.iterator]() method simply gives back the object itself. So for a generator object called it, it[Symbol.iterator]() === it is true.
The table below compares a generator object with an array. Read each row as a question, then the answer for each one.
| Question | Generator object | Array |
|---|---|---|
| When are the values made? | One at a time, only when someone asks for the next one. | Usually all at once. They are already stored. |
| Can it go on forever? | Yes, as long as the code that asks for values stops at some point. | No. An array always has a fixed number of items. |
| Can you loop over it twice? | No. Once it is used up, the same object stays finished. | Yes. An array keeps its items. |
| What can you do with it? | Call next(), or loop over it with for...of and spread. | Loop over it, and also use array methods like map. |
function* colors() { yield "red"; yield "blue";} const list = colors();console.log(list.next()); // { value: "red", done: false }console.log([...colors()]); // ["red", "blue"]console.log([...list]); // ["blue"] because red was already consumedLine 6 makes a generator object called list. Line 7 calls list.next() once and gets red. Line 8 spreads a brand-new generator object from colors(), so it gets both colors. Line 9 spreads list, the object from line 6.
Line 9 surprises many people. It prints only ["blue"]. list already gave out red on line 7, so the spread starts from where list paused, not from the beginning. To start again, call the generator function again to get a fresh object.
function* chores() { yield "wash"; yield "read";} const today = chores();console.log([...today].join(", "));console.log([...today].join(", ") || "nothing left");Line 6 makes one generator object, today. Line 7 spreads it and prints wash, read. That uses it up. Line 8 spreads the same object again. It has no values left, so join gives an empty string, and the || "nothing left" part prints nothing left instead.
If some code may need the values again, keep the generator function and call it again for a fresh object.
In the playground below, you send commands to one live generator. Each button calls a method on the same object, so the order you press them in matters. Reset makes a new object. You'll learn about return() and throw() in the next section, but you can try them now.
function* task() { console.log("start"); try { yield "first"; console.log("after first"); yield "second"; } catch (error) { console.log("caught " + error.message); } finally { console.log("cleanup"); } return "done";}Results you got back
No commands yet.
Printed inside the generator
Nothing printed yet.
The generator is made, but none of its code has run. Press next() to start it.
next(), return(), and throw()
next() is the normal "go on" command. A generator object has two more methods. Both act on a generator that is paused at a yield:
return(value)tells the generator to finish now. It gives back{ value, done: true }. If the generator paused inside atrythat has afinallyblock, thefinallycode still runs first. Ayieldinside thatfinallyblock could even pause it once more before it finishes.throw(error)throws the error at the pausedyield, as if that line had thrown it. Atry...catcharound thatyieldcan catch it. If the generator has not started yet, there is no pausedyield, so the error is thrown straight back to the code that calledthrow().
Both methods work well with the try, catch & finally rules you already know. Put clean-up code, like closing a connection, in a finally block. Then it runs even when a loop stops early.
Only the function* form makes a generator. There is no arrow function version. Also, you can't use new with a generator function: new story() throws a TypeError.
function* makeReceipt() { yield "item"; return "total";} const receipt = makeReceipt();console.log(JSON.stringify(receipt.next()));console.log(JSON.stringify(receipt.next()));console.log([...receipt]);Line 2 yields item, and line 3 returns total. Line 7 prints the first result, with done: false. Line 8 prints total with done: true. Line 9 spreads what is left and prints an empty array, [].
for...of and spread only collect values from yield. They stop at done: true, so they never see a generator's final return value.
function* openConnection() { try { yield "connected"; } finally { console.log("closed"); }} const connection = openConnection();console.log(connection.next().value);console.log(JSON.stringify(connection.return("stopped")));Line 10 calls next(). The code runs to the yield on line 3 and pauses inside the try. Line 10 prints connected.
Line 11 calls return("stopped"). The generator finishes from that paused yield, but first its finally block runs and prints closed. Then line 11 prints the result: {"value":"stopped","done":true}.
yield. return() wakes it up only to finish: the finally block prints closed, then the result is done: true.function* safeTask() { try { yield "waiting"; } catch (error) { yield "recovered: " + error.message; }} const task = safeTask();console.log(task.next().value);console.log(task.throw(new Error("retry" )).value);Line 10 calls next(), and the generator pauses at the yield on line 3. Line 10 prints waiting.
Line 11 calls throw() with an error whose message is retry. The error is thrown at the paused yield on line 3, so the catch on line 4 catches it. Line 5 yields recovered: retry, and line 11 prints that value.
yield* delegation
INTERACTIVEyield* (yield with a star) means: "hand out every value from this other iterable, one at a time, and then carry on here." Passing the work to another iterable like this is called delegation. It works with any iterable: arrays, strings, sets, your own iterables, and other generators.
When the other iterable is a generator, there is one more thing. That generator's return value becomes the result of the yield* expression, so you can save it in a variable.
A show host talks for a while, then hands the microphone to a guest. The host waits until the guest is done. Then the host takes the microphone back and carries on. The guest's last words are said only to the host, and the host can choose to repeat them to the audience.
- In real life: The host starts the show
- In JavaScript: The outer generator yields its own values
- In real life: The guest takes the microphone
- In JavaScript:
yield* guest()hands out each of the guest's values - In real life: The guest's last words, said only to the host
- In JavaScript: The other generator's
returnvalue - In real life: The host takes the microphone back
- In JavaScript: The code after
yield*runs
function* names() { yield* ["Asha", "Ben"]; yield "done";} console.log([...names()].join(", "));Line 2 uses yield* on the array ["Asha", "Ben"]. It hands out Asha, then Ben. Then line 3 yields done. So line 6 prints Asha, Ben, done.
yield* passes out every item of the array, one at a time. When the array has no more items, names() goes on to line 3 and yields "done".An array has no return value, so here the yield* expression just gives undefined.
The playground below has two examples:
- Story team: a generator saves another generator's return value and then yields it.
- Flatten arrays:
yield*is used inside a function that calls itself.
function* leaf(name) { yield name + ": start"; yield name + ": end"; return name + " finished";} function* storyTeam() { yield "host intro"; const finalLine = yield* leaf("guest"); yield "host heard " + finalLine; return "host finished";} console.log([...storyTeam()].join(" | "));host introguest: startguest: endhost heard guest finished- The last
next()call gives back{ value: "host finished", done: true }
Line 9 uses yield* leaf("guest") to hand the microphone to the guest generator. When the guest returns, its last words are saved in finalLine, and line 10 yields them.
yield* works with any iterable. When the other iterable is a generator, yield* also gives back its return value.The flatten example uses the same idea as the Recursion lesson. Each nested array gets its own flatten call. yield* passes the values from the deeper call out to the code that loops over the result.
Infinite sequences
INTERACTIVEA generator makes each value only when someone asks for it. This is called being lazy. Because of this, a generator can describe an infinite sequence: a list of values that never ends. An array could never store one. Some examples: IDs, timestamps, retry attempts, Fibonacci numbers, pages from a long search, or events as they happen.
The rule is simple. An endless generator is safe only when the code that asks for values stops asking.
The take helper in the playground does that stopping. take(ids(), 3) asks ids() for three values, hands them out, and then stops.
ids() could count forever, but it only makes a number when asked. Here take asks three times and then stops, so 4 is never made.function* ids() { let id = 1; while (true) { console.log("compute id " + id); yield id; id += 1; }} function* take(iterable, limit) { const iterator = iterable[Symbol.iterator](); for (let count = 0; count < limit; count += 1) { const result = iterator.next(); if (result.done) return; yield result.value; }} console.log([...take(ids(), 3)].join(", "));Values: 1, 2, 3
Log:
compute id 1compute id 2compute id 3
take(ids(), 3) makes exactly three IDs.
[...ids()] or [...fibonacci()] on purpose: they would never finish.[...ids()], Array.from(ids()), and a for...of loop with no break all keep asking for values until they get done: true. An endless generator never gives done: true, so they never stop. Put a limit in front first, like take(ids(), 3).
[...take(ids(), 3)]orders.map(order => order.id)[...ids()]function* paidOrderIds(orders) { ... }orders.filter(order => order.paid)Array.from(fibonacci())
Put each line of code in its group. Lazy: values are made only when asked. Eager: the whole result is made right away. Unsafe: it asks an endless generator for every value.
Where you'll use generators
Generators are not for every loop. Use one when:
- the values come one after another, over time,
- building the whole array first would waste memory or work, or
- a function that calls itself (recursion) needs to pass results back out as it finds them.
function* makeIds(start) { let id = start; while (true) { yield "order-" + id; id += 1; }} function* take(iterable, limit) { let count = 0; for (const value of iterable) { if (count === limit) return; count += 1; yield value; }} console.log([...take(makeIds(100), 3)].join(", "));Line 1 starts makeIds, a generator that makes IDs. Line 2 keeps the next number in a local variable, id. The loop on line 3 never ends. Each time round, line 4 yields one ID, and line 5 adds 1.
Line 18 uses take to keep only three IDs, so it prints order-100, order-101, order-102. This take is written with for...of. When a value arrives after the limit, line 12 returns. That ends the for...of loop, and the loop closes the endless ID maker.
function* pageResults(pages) { for (const page of pages) { console.log("request page"); yield* page; }} function* take(iterable, limit) { let count = 0; for (const value of iterable) { if (count === limit) return; count += 1; yield value; }} const pages = [["Asha", "Ben"], ["Chen", "Di"]];console.log([...take(pageResults(pages), 3)].join(", "));pageResults loops over the pages. Line 3 prints request page; it stands in for asking a server for the next page. Line 4 uses yield* to hand out that page's names one by one, without first joining all the pages into one big array.
Line 18 asks for three names. The first page gives Asha and Ben. Then the second page is requested, and Chen is the third name. So the output is two request page lines, then Asha, Ben, Chen. The name Di is never printed.
function* trafficLight() { let light = "red"; while (true) { yield light; light = light === "red" ? "green" : light === "green" ? "yellow" : "red"; }} const lights = trafficLight();console.log(lights.next().value);console.log(lights.next().value);console.log(lights.next().value);console.log(lights.next().value);A state machine is something that is always in one state, and has rules for moving to the next one. A traffic light is a good example: red, then green, then yellow, then red again.
Line 2 starts with light set to red. Line 4 yields the current color and pauses. When next() is called again, line 5 picks the next color. Because light is kept between pauses, lines 10 to 13 print red, green, yellow, and red. This code only describes the order of the colors. It does not control a real traffic light.
- Use an array when the data is small, has an end, and you need it more than once.
- Use a generator when the code that asks may stop early, or when each next item takes real work to make.
- Put the limit right next to an endless generator, like
take(source, limit). - A generator object works only once. Call the generator function again to start over.
const orders = [ { id: 1, paid: true }, { id: 2, paid: false }, { id: 3, paid: true },]; function* paidOrderIds(items) { for (const order of items) { if (order.paid) yield order.id; }} console.log([...paidOrderIds(orders)].join(", "));Lines 1 to 5 list three orders. Lines 7 to 11 write paidOrderIds. Line 8 loops over the orders, and line 9 yields an order's id only if it is paid. Line 13 prints 1, 3.
For a short array that is already in memory, orders.filter(...).map(...) works just as well. The generator version helps when orders arrive one page at a time, or when the code that asks may stop after the first match.
Other places where generators show up:
- Splitting text into words: yield one word at a time.
- Walking a tree, like folders or a menu: yield each file, folder, or menu item as you find it.
- Making test data: yield as many test cases as the test needs.
- Chains of steps: combine
filter,map, andtakewithout making a new array between each step.
Common mix-ups
"Calling a generator runs it."
No. Calling it only makes a generator object. The code waits for the first next().
"A generator object is like an array I can reuse."
No. It hands out its values one at a time, and only once. When it is used up, it stays finished. Call the generator function again for a fresh object.
"return() skips the clean-up code."
No. It runs any waiting finally block first. That is why a generator can clean up when a loop stops early.
"yield* only works with generators."
No. It works with any iterable. The extra part with a generator is that yield* gives back that generator's final return value.
"Lazy code is always safer."
No. Lazy code only saves work if the code that asks for values stops. Spreading an endless generator is a bug: it never finishes.
The table below compares the three commands you can send to a generator. Read each row as: "when I call this, where does the generator carry on, and what do I get back?"
| When you call | Where the generator carries on | What you get back |
|---|---|---|
next() | At the start of its code, or right after the paused yield. | The next yielded value with done: false, or done: true once the code has finished. |
return(value) | At the paused yield, but only to finish early. | Usually { value, done: true }, after any finally block has run. |
throw(error) | At the paused yield, as if that line threw the error. | If the generator catches it, its next result. If not, the error is thrown back to you. |
Practice: generators
5 EXERCISESTry each exercise before you open the hints. The first ones ask you to predict the output. The later ones ask you to write and fix generators.
This program logs three values. Write them in order, separated by commas.
function* letters() {
yield "A";
yield "B";
}
const it = letters();
console.log(it.next().value);
console.log(it.next().done);
console.log(it.next().done);The first call yields A, so .value is A. The second call yields B, so .done is false. The third call finds the code finished, so .done is true.
Write a generator that yields the odd numbers 1, 3, 5, and so on, up to limit.
function* odds(limit) {
// yield 1, 3, 5 up to limit
}
console.log([...odds(5)].join(","));function* odds(limit) {
for (let n = 1; n <= limit; n += 2) {
yield n;
}
}
console.log([...odds(5)].join(","));The loop visits 1, 3, and 5. Each yield pauses and hands one number to the spread. The spread stops when the generator finishes.
This code never finishes. What is wrong with it, and how can you limit it safely?
function* ids() {
let id = 1;
while (true) yield id++;
}
console.log([...ids()].slice(0, 3));function* ids() {
let id = 1;
while (true) yield id++;
}
function* take(iterable, limit) {
let count = 0;
for (const value of iterable) {
if (count === limit) return;
count += 1;
yield value;
}
}
console.log([...take(ids(), 3)].join(","));The bug is [...ids()]. The spread asks the endless generator for every value before .slice can run, so it never finishes. take(ids(), 3) stops after three values.
Predict what this program prints.
function* one() {
yield "x";
return "done";
}
function* two() {
const message = yield* one();
yield message;
}
console.log([...two()].join("|"));two hands out x from one. Then yield* gives back the return value of one, which is done, and line 6 stores it in message. Line 7 yields it, so the output is x|done.
Write a generator that takes a list of task objects like { title: "Fix nav", tags: ["ui"] }. It should yield only the titles of tasks whose tags include a given tag.
function* matchingTasks(tasks, tag) {
for (const task of tasks) {
if (task.tags.includes(tag)) {
yield task.title;
}
}
}This hands out the matching titles one at a time, without building a second array of tasks. The code that asks can stop after the first few matches. If the list has an end, it can also collect them all with spread.
Quiz: check your understanding
7 QUESTIONSPick an answer, then read the explanation for every choice. The wrong choices show common mistakes with generators.
Question 1 of 7When does the code inside a generator function start running?
Choose an answer to see the explanation.
Question 2 of 7What does this code print?
Read the code, then predictfunction* colors() { yield "red"; yield "blue"; } const list = colors(); console.log(list.next().value); console.log(list.next().done); console.log(list.next().done);Choose an answer to see the explanation.
Question 3 of 7Which statement about generator objects is true?
Choose an answer to see the explanation.
Question 4 of 7What does
return("stop")do to a paused generator?Choose an answer to see the explanation.
Question 5 of 7What does
yield* otherIterabledo?Choose an answer to see the explanation.
Question 6 of 7What does this
yield*program print?Read the code, then predictfunction* inner() { yield "a"; return "done"; } function* outer() { const final = yield* inner(); yield final; } console.log([...outer()].join(","));Choose an answer to see the explanation.
Question 7 of 7Which line should you never run with an endless generator like
ids()?Choose an answer to see the explanation.
Key takeaways
function*makes a generator function. Calling it gives you a generator object and runs none of its code.- Each
next()runs the code until the nextyield, areturn, or an error. - A generator object is both an iterator and an iterable, so it works with
for...ofand spread. It works only once. return()finishes early and still runsfinally.throw()throws an error at the pausedyield.yield*hands out every value from another iterable. With a generator, it also gives back that generator's return value.- An endless generator is safe only when something like
takestops asking for values.
Remember the one-liner.
A generator is a function that can pause, and it makes each value only when you ask for it.
Up next: Iterator helpers. They give any iterator lazy map, filter, take, and more.