Generators for control flow
Use generators as paused control-flow tools: send values with next, build a promise runner, clean up with finally, and coordinate cooperative coroutines.
- 01Talk both ways with a generatorPredict what happens when
next(value),throw(error), orreturn(value)starts a pausedyieldagain. - 02Wait for promises with a runnerStep through a tiny promise runner and compare it with the same code written with
async/await. - 03Take turns and clean upUse
finallyto clean up when a caller stops early, and trace tasks that take turns at eachyield.
A generator is a program you can pause
You already know that a generator can give you values one at a time. In this lesson you use the same pause-and-continue trick for control flow. Control flow means the order in which code runs. Here it answers three questions:
- Which piece of code runs next?
- What value does a paused line get when it starts again?
- What cleanup runs when the caller stops early?
A quick reminder of the basics. A generator function is written with function*. Calling it does not run its body. It gives you an iterator: an object with a next() method. Each next() call runs the body up to the next yield and pauses there. It gives back a small object, { value, done }.
The new part is this: the caller can also send a value back in to the paused yield. The caller is the code that calls next().
Picture two people with walkie-talkies. One asks, “What is your name?” and waits. The other replies, “Ada.” The first person goes on from that exact point and uses the answer. A generator does the same thing. The waiting point is a yield.
- In real life: One person speaks and waits
- In JavaScript: The generator reaches
yieldand pauses - In real life: The other person replies
- In JavaScript: The caller runs
next(answer) - In real life: The reply is heard where the first person stopped
- In JavaScript: The answer becomes the value of the paused
yield
You will start with a tiny conversation. Then you will build a promise runner, stop a generator early, let tasks take turns, and step through a list of states. The goal is not to replace async/await. The goal is to see what a paused program can do.
Sending values with next()
STEP THROUGHThe code yield "question" does two jobs:
- It sends
"question"out to the caller, and the generator pauses. - Later the caller runs
next(answer). The generator starts again, and the wholeyield "question"part now stands foranswer.
yield on line 2 sends the question out and pauses. Line 8 sends "Ada" back in, so name gets "Ada".The first next() call is different. It only starts the body. The generator is not paused at a yield yet, so there is nowhere for a value to go. That is why the first call's argument, next("ignored"), is thrown away. Step through the code below and change the answers to see this.
Predict each printed value. Then step through to see how next(value) sends an answer back to the paused yield.
script
function* interview() { const name = yield "What is your name?"; const color = yield "Hi " + name + ", favorite color?"; return name + " likes " + color;}console.log(chat.next("ignored").value);console.log(chat.next("Ada").value);console.log(chat.next("blue").value);The value you pass to next(v) becomes the value of the yield where the generator is paused right now. Before the first yield, nothing is paused, so nothing receives v.
A generator has two more methods that start it again from the paused yield:
throw(error)raises an error at the pausedyield. Atry/catchinside the generator can catch it.return(value)asks the generator to finish now. Anyfinallyblock still runs on the way out.
function* checkout() { try { const address = yield "address?"; return "ship to " + address; } catch (error) { return "ask again"; }} const order = checkout();console.log(order.next().value);console.log(order.throw(new Error("missing"))); const secondOrder = checkout();secondOrder.next();console.log(secondOrder.return("cancelled"));Line 12 sends an error into the paused yield. The generator catches it and returns "ask again". Line 16 stops a second generator before it gets an address. So it gives back { value: "cancelled", done: true }.
Both methods are requests from the caller. The generator can catch the error, and its finally block can run cleanup. Neither method rewinds the body or starts it over.
Generators that wait for promises
INTERACTIVEA promise is a value that arrives later. It fulfills when the value arrives, or rejects when something goes wrong. Before async/await existed, libraries such as co used generators to make promise code read from top to bottom. It worked like this:
- The generator yields a promise and pauses.
- A small function called a runner waits for that promise.
- If the promise fulfills, the runner calls
next(result). The pausedyieldgets the result. - If the promise rejects, the runner calls
throw(error). The error appears at the pausedyield.
Picture a personal assistant. You stop your plan and hand over an errand. The assistant goes out, waits, and comes back with the result or with the problem. Then you go on with your plan from the exact point where you stopped.
- In real life: You hand over an errand
- In JavaScript: The generator yields a promise
- In real life: The assistant waits
- In JavaScript: The runner waits for the promise to fulfill or reject
- In real life: The assistant brings the result back
- In JavaScript: The runner calls
next(result) - In real life: Bad news is brought back too
- In JavaScript: The runner calls
throw(error)
next(user) or throw(error).The runner in the lab below follows three rules:
- It starts the generator with
next(). - It wraps each yielded value in
Promise.resolve(...). That turns any value into a promise, so the runner can always wait the same way. - When the promise is done, it calls exactly one method:
next(value)if the promise fulfilled, orthrow(error)if it rejected.
function run(genFn) { const it = genFn(); function step(method, value) { const result = it[method](value); if (result.done) return Promise.resolve(result.value); return Promise.resolve(result.value).then( (value) => step("next", value), (error) => step("throw", error) ); } return step("next");} function* loadProfile() { const user = yield getUser(); const posts = yield getPosts(user.id); return user.name + " has " + posts.length + " posts";}async function loadProfile() { const user = await getUser(); const posts = await getPosts(user.id); return user.name + " has " + posts.length + " posts";}The runner waits for each yielded promise. When it fulfills, the runner sends the value back in with next(value).
co did before async/await existed. Today async/await is built into JavaScript.The steps happen one after another. The second request must wait for the first, because the generator needs user.id to ask for the posts. If two requests don't need each other, run them together with Promise.all inside an async function instead.
Today, async/await does this job for you. The language itself pauses the function, waits for the promise, and lets you use a normal try/catch. That code is easier to read, debug, and check for type errors. So in everyday app code, use async & await.
The generator runner is still worth learning. It shows each handoff clearly: pause, wait, send the value back, go on. Some libraries also still need a caller that controls each step.
Cleanup with finally
INTERACTIVEA generator may hold something that must be closed when you are done with it. Examples are a file reader, a lock, or a subscription to updates. Put the closing code in a finally block. The generator runs that finally block when the caller stops early, in either of two ways:
- The caller calls
return()on the generator. - A
for...ofloop over the generator ends withbreak.
for...of loop ends with break, the loop calls return() for you. return() makes the generator run its finally block.Good cleanup finishes right away, without pausing. For example, it removes a subscription, resets a flag, or closes a reader before the caller's code goes on. Then stopping early always works the same way, even if the caller never calls next() again.
function* readResource() { try { console.log("open"); yield "chunk 1"; yield "chunk 2"; } finally { console.log("close start"); yield "paused in cleanup"; console.log("closed"); }}open
close start
closed
first: {"value":"chunk 1","done":false}
return: {"value":"paused in cleanup","done":false}
next: {"value":"stop","done":true}
Calling return() starts the cleanup. The yield inside finally pauses the cleanup, so return() comes back with done: false. The next next() call finishes it. Avoid a yield in real cleanup code.
yield inside finally on purpose, to show what happens. In real cleanup code, don't do this.The lab has a yield inside finally on purpose. JavaScript allows it, and it shows that cleanup can pause too. But it is a trap: the reader or lock may stay open until someone starts the generator again. Real cleanup code should finish without a yield.
This uses the same finally you met in try, catch & finally. The new part is that the caller can ask the generator to stop early.
A for...of loop uses the values the generator yields. It ignores the generator's final return value. When the loop ends with break, JavaScript first closes the generator by calling its return() method. That is why finally runs. Readers and subscriptions that you can cancel depend on this rule.
Coroutines: taking turns on purpose
INTERACTIVEA coroutine is a piece of code that takes turns with other pieces of code. It decides for itself when to pause and let another one run. With generators, each task runs until it reaches a yield. Then a scheduler, the code that picks who goes next, can start a different task.
In chess, a player gets a turn, thinks, and moves. Only then does the other player act. Coroutines work like that: each task hands control back by itself when its move is done.
- In real life: A player thinks and makes a move
- In JavaScript: A task runs until its next
yield - In real life: The clock passes to the other player
- In JavaScript: The scheduler starts another generator
- In real life: No one grabs a piece in the middle of a move
- In JavaScript: No task is stopped halfway through its code
function* saveInPieces() { yield "save title"; yield "save body"; return "saved";} const task = saveInPieces();console.log(task.next().value);console.log(task.next().value);console.log(task.next());The first two calls print "save title" and "save body". The third call prints { value: "saved", done: true }. The generator has finished, so it gives its return value instead of another turn.
A task that takes turns must pause at good places. Yielding after each small piece of work lets the scheduler switch tasks often. But a long loop between two yields still freezes the page until that loop ends. Generators never stop your code in the middle for you.
The lab below uses a queue: a list where you take from the front and add to the back. The picture shows one turn.
yield, and puts it at the back. So the turns go paint, cook, music, then paint again.function* task(name, count) { for (let i = 1; i <= count; i++) { yield name + " step " + i; }} const tasks = [task("paint", 2), task("cook", 3), task("music", 2)];while (tasks.length > 0) { const current = tasks.shift(); const result = current.next(); if (!result.done) tasks.push(current);}function* trafficLight() { while (true) { yield "green"; yield "yellow"; yield "red"; }} const light = trafficLight();console.log(light.next().value);console.log(light.next().value);console.log(light.next().value);console.log(light.next().value);Click Run next turn to start.
Each task runs until its own yield. Then the scheduler puts it at the back of the queue. No task is stopped in the middle of its turn.
The tasks do not run at the same time. Only one runs at a time, and they take turns. This is a clear way to write step-by-step work: animation scripts, parsers that read text piece by piece, or teaching tools that pause at named points.
Don't mix up this small queue with the browser's event loop. The browser runs timers, clicks, and screen updates around your code by itself. A generator scheduler runs only when your own code calls next(). Once a slow step has started, the scheduler can't make the page respond until that step ends.
Generators as state machines
A state is the stage something is in right now. A state machine moves through a fixed list of states, one step at a time. A traffic light is one: green, yellow, red, then green again. A generator fits this well:
- Each
yieldis one state. - Each
next()call moves one step forward. - The generator remembers which state it is in, so you don't need a variable for it.
function* orderState() { yield "cart"; yield "paid"; yield "shipped";} const order = orderState();console.log(order.next().value);console.log(order.next().value);console.log(order.next().value);Lines 8 to 10 print cart, paid, and shipped, in that order. The generator remembers where the order is. It does not check any rules, though. A real shop still needs code that decides whether the payment is allowed.
Use this pattern when the states always come in the same order and the caller decides when to move on. Use a state machine library or a switch on the current state when you need more: branches, saving the state, or many separate states at once.
The table compares a generator with a runner and an async function. Read each row as a question, then compare the two answers.
| What you want to know | Generator with a runner | Async function |
|---|---|---|
| Which word pauses it? | yield pauses a generator. | await pauses an async function. |
| Who starts it again? | The caller does, with next, throw, or return. | JavaScript does, when the awaited promise fulfills or rejects. |
| What do you get back? | Each step gives an object like { value, done }. | The call gives one promise that fulfills or rejects. |
| When is it the best choice today? | For custom loops, step-by-step replays, teaching, and tasks that take turns. | For everyday code that waits for promises. |
Generators are great for custom loops, step-by-step replays, tasks that take turns, and small state machines. Code that waits for promises usually belongs in an async function today.
The generator above does not guard its steps. A real state machine may refuse shipped before paid, save the current state, or pick between two next states. The generator gives you a readable order of steps. The shop's rules still belong in your app code.
Where you'll use this
SORTERYou will meet generator control flow in libraries, test tools, parsers, animation demos, and job interviews. The skill is not to turn every function into a generator. The skill is to spot when a pause point makes the code easier to follow.
On a website, a generator can give you one page of results at a time, replay the steps of a long form, or drive a teaching demo. It is a poor fit for plain data formatting. A hand-written promise runner is usually a poor fit for new app code, too.
- A step-by-step form asks one question, waits for the answer, then asks the next
- Load JSON with
await fetch(...)in a real app - Turn cents into a price string right away
- A small scheduler lets tasks take turns
- Try a request, catch errors, and await a delay before trying again
- Add all numbers in an array and return the total
Sort each job by the simplest kind of function that does it. Each explanation says when a generator helps and when it is more than you need.
Common misconceptions
“The first next(value) sends value into the first yield.”
It can't. The body has not reached a yield yet, so the first argument is thrown away. The second call is the first one that can send a value in.
“yield and await are the same thing.”
Both pause a function, but they are different tools. yield pauses until the caller calls next(). await works only inside an async function, and it goes on by itself when the promise is done.
“A scheduler can stop a task whenever it wants.”
Not with generators. A task runs until it reaches its own yield. A long loop with no yield still blocks every other task.
“return() skips finally.”
No. return() asks the generator to finish, and finally blocks still run. That is why finally is the right place for cleanup.
“The generator runner is how JavaScript runs await.”
No. The runner shows the same steps, and libraries used it before async/await existed. But JavaScript defines async functions on their own, without a runner.
Practice: control the pause
5 EXERCISESPredict exactly what the program logs, in order.
function* demo() {
const answer = yield "question";
console.log(answer);
}
const it = demo();
console.log(it.next("first").value);
it.next("second");The first log prints the yielded value, question. Then it.next("second") starts the paused yield again, so answer is "second" and the generator logs it.
Which word shows that the generator ran its cleanup, even though the loop stopped early?
function* files() {
try {
yield "file";
} finally {
console.log("closed");
}
}
for (const file of files()) {
console.log(file);
break;
}The loop prints file. Then break closes the generator, which runs the finally block. So closed shows that cleanup happened.
A tiny runner already uses next(value) when a promise fulfills. Which method should it use to send a rejection back into the generator?
return Promise.resolve(result.value).then(
(value) => step("next", value),
(error) => step("throw", error)
);Use throw(error) when the promise rejects. Then the generator's own try/catch can handle the error at the paused yield.
In what order does the scheduler print the values?
function* one() {
yield "A1";
yield "A2";
}
function* two() {
yield "B1";
}
const queue = [one(), two()];
while (queue.length) {
const current = queue.shift();
const result = current.next();
if (!result.done) {
console.log(result.value);
queue.push(current);
}
}one yields A1 and goes to the back. two yields B1 and goes to the back. Then one yields A2. On its next turn two is finished, so it is not put back. one is finished on its last turn too, so nothing else prints.
For a real profile loader that waits for network promises, which tool should you usually choose today?
For loading promises in modern app code, choose an async function with await. The generator runner helps you understand the steps, but everyday promise code is clearer with the built-in syntax.
Quiz: check your understanding
7 QUESTIONSPick an answer before you look. Then read every explanation. The wrong answers are common generator mistakes.
Question 1 of 7What happens to the argument of the very first
next(value)call?Choose an answer to see the explanation.
Question 2 of 7What does this two-way generator print?
Read the code, then predictfunction* demo() { const x = yield "ready"; console.log(x); } const it = demo(); console.log(it.next("ignored").value); it.next("Ada");Choose an answer to see the explanation.
Question 3 of 7In the promise runner, why does the success handler call
step("next", value)?Choose an answer to see the explanation.
Question 4 of 7What does
it.throw(error)do to a paused generator?Choose an answer to see the explanation.
Question 5 of 7What does this early break print?
Read the code, then predictfunction* g() { try { yield "work"; } finally { console.log("cleanup"); } } for (const value of g()) { console.log(value); break; }Choose an answer to see the explanation.
Question 6 of 7What is the main rule for the tasks in this lesson's scheduler?
Choose an answer to see the explanation.
Question 7 of 7Which tool should you usually choose today for everyday code that waits for promises?
Choose an answer to see the explanation.
Key takeaways
next(value)sendsvalueinto the pausedyield. The first call's argument is thrown away.throw(error)raises an error at the pausedyield.return(value)finishes the generator, andfinallyblocks still run.- A promise runner waits for each promise, then calls
next(result)orthrow(error). Modern code usually usesasync/awaitinstead. - Coroutines take turns: each task runs until it reaches its own
yield. - Generators help with custom loops, step-by-step replays, state machines, and small schedulers. They are not for every function.
Remember the one-liner.
A generator is a paused program that the caller can start again with a value, an error, or a request to finish.
Up next: Why modules?