completefrontend
Open lab My progress
THE JAVASCRIPT FIELD GUIDE

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.

By the end, you can
  • 01
    Talk both ways with a generatorPredict what happens when next(value), throw(error), or return(value) starts a paused yield again.
  • 02
    Wait for promises with a runnerStep through a tiny promise runner and compare it with the same code written with async/await.
  • 03
    Take turns and clean upUse finally to clean up when a caller stops early, and trace tasks that take turns at each yield.

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().

Real-life analogynext(value) is a two-way walkie-talkie

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 yield and 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 THROUGH

The code yield "question" does two jobs:

  1. It sends "question" out to the caller, and the generator pauses.
  2. Later the caller runs next(answer). The generator starts again, and the whole yield "question" part now stands for answer.
The generator is paused at the yield on line 2. The question What is your name? goes out to the caller. Then the caller runs next with Ada, and Ada comes back in, so name is Ada.yieldpaused on line 2name = "Ada"1. goes out"What is your name?"next("Ada")2. comes back in
The 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.

Step through a two-way generator conversation
Step 0 of 9Ready
Your turn: follow the blue line

Predict each printed value. Then step through to see how next(value) sends an answer back to the paused yield.

Running in
  1. script
Next: line 6
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);
CallStoreChangeResultBlue line: runs next. Ran: already executed.
Recent returnsNothing yet. Start with the blue line.
Change the answers sent by lines 8 and 9.

Changing the answers starts the replay over. Notice that the argument on line 7 is still thrown away.

A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.
The exact rule

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 paused yield. A try/catch inside the generator can catch it.
  • return(value) asks the generator to finish now. Any finally block still runs on the way out.
throw() sends in an error; return() stops earlyPop out in the code editor (opens in a new tab)JavaScript
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

INTERACTIVE

A 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:

  1. The generator yields a promise and pauses.
  2. A small function called a runner waits for that promise.
  3. If the promise fulfills, the runner calls next(result). The paused yield gets the result.
  4. If the promise rejects, the runner calls throw(error). The error appears at the paused yield.
Real-life analogyA promise runner is a personal assistant

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)
Step 1, the generator yields getUser() and pauses. Step 2, the runner waits for that promise. If the promise fulfills, the runner calls next with the user. If it rejects, the runner calls throw with the error.yield getUser()1. generator pausespromise2. runner waitsif it fulfillsnext(user)throw(error)if it rejects
The generator hands a promise to the runner and pauses. When the promise is done, the runner starts the generator again with 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, or throw(error) if it rejected.
Promise runner lab
A generator promise runnerPop out in the code editor (opens in a new tab)JavaScript
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";}
The same steps with async/awaitPop out in the code editor (opens in a new tab)JavaScript
async function loadProfile() {  const user = await getUser();  const posts = await getPosts(user.id);  return user.name + " has " + posts.length + " posts";}
What happened, in ordersuccess
final resultClick Run
Try it yourself

The runner waits for each yielded promise. When it fulfills, the runner sends the value back in with next(value).

This is a tiny version of what libraries like 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

INTERACTIVE

A 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...of loop over the generator ends with break.
break stops a for...of loop early. The loop then calls return() on the generator for you, or you can call return() yourself. return() makes the generator run its finally block, where the cleanup is.breakloop stops earlyreturn()called by the loopor by youfinallycleanup runs
When a 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.

Cleanup lab: stopping early runs finally
A generator that opens, reads, and closesPop out in the code editor (opens in a new tab)JavaScript
function* readResource() {  try {    console.log("open");    yield "chunk 1";    yield "chunk 2";  } finally {    console.log("close start");    yield "paused in cleanup";    console.log("closed");  }}
What was loggedreturn

open

close start

closed

first: {"value":"chunk 1","done":false}

return: {"value":"paused in cleanup","done":false}

next: {"value":"stop","done":true}

Try it yourself

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.

This code has a yield inside finally on purpose, to show what happens. In real cleanup code, don't do this.
Don't yield inside cleanup

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

INTERACTIVE

A 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.

Real-life analogyCoroutines are chess players taking turns

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
Each yield gives one turn backPop out in the code editor (opens in a new tab)JavaScript
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.

Before a turn, the tasks list holds paint, cook, music. paint is at the front, so it runs to its next yield. Then it goes to the back. After the turn, the list holds cook, music, paint.beforepaintcookmusic1 turn, then to the backaftercookmusicpaint
The scheduler takes the task at the front, runs it to its next yield, and puts it at the back. So the turns go paint, cook, music, then paint again.
Coroutine lab: tasks take turns
A scheduler that gives each task a turnPop out in the code editor (opens in a new tab)JavaScript
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);}
A traffic light as a state machinePop out in the code editor (opens in a new tab)JavaScript
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);
Turns so far0/7

Click Run next turn to start.

Traffic light statesgreen → yellow → red → green → yellow → red
Step 0 of 7turns

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.

This scheduler helps you picture tasks taking turns, and it works for small jobs. It does not replace the browser's own scheduling tools.

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 yield is 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.
An order moves through three statesPop out in the code editor (opens in a new tab)JavaScript
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.

Generator with a runner vs async function
What you want to knowGenerator with a runnerAsync 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

SORTER

You 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.

Which kind of function fits the job?
  • 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
Try it yourself
0 of 6 correct

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.

Choose a category for every card. You can change an answer at any time; Reset clears them all.

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 EXERCISES
Exercise 1 · Warm-upPredict the two-way output

Predict exactly what the program logs, in order.

Starter codePop out in the code editor (opens in a new tab)JavaScript
function* demo() {
  const answer = yield "question";
  console.log(answer);
}
const it = demo();
console.log(it.next("first").value);
it.next("second");

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · PracticeFind the cleanup

    Which word shows that the generator ran its cleanup, even though the loop stopped early?

    Starter codePop out in the code editor (opens in a new tab)JavaScript
    function* files() {
      try {
        yield "file";
      } finally {
        console.log("closed");
      }
    }
    for (const file of files()) {
      console.log(file);
      break;
    }

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticePatch the runner

      A tiny runner already uses next(value) when a promise fulfills. Which method should it use to send a rejection back into the generator?

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticeTrace the turns

        In what order does the scheduler print the values?

        Starter codePop out in the code editor (opens in a new tab)JavaScript
        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);
          }
        }

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengeChoose the plain tool

          For a real profile loader that waits for network promises, which tool should you usually choose today?

          Answer, then press Check. Spacing and letter case don’t matter.

            Quiz: check your understanding

            7 QUESTIONS

            Pick an answer before you look. Then read every explanation. The wrong answers are common generator mistakes.

            Lesson quiz · 7 questionsScore: first tries count
            1. Question 1 of 7What happens to the argument of the very first next(value) call?

              Choose an answer to see the explanation.

            2. Question 2 of 7What does this two-way generator print?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              function* 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.

            3. Question 3 of 7In the promise runner, why does the success handler call step("next", value)?

              Choose an answer to see the explanation.

            4. Question 4 of 7What does it.throw(error) do to a paused generator?

              Choose an answer to see the explanation.

            5. Question 5 of 7What does this early break print?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              function* g() {
                try {
                  yield "work";
                } finally {
                  console.log("cleanup");
                }
              }
              for (const value of g()) {
                console.log(value);
                break;
              }

              Choose an answer to see the explanation.

            6. Question 6 of 7What is the main rule for the tasks in this lesson's scheduler?

              Choose an answer to see the explanation.

            7. 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) sends value into the paused yield. The first call's argument is thrown away.
            • throw(error) raises an error at the paused yield. return(value) finishes the generator, and finally blocks still run.
            • A promise runner waits for each promise, then calls next(result) or throw(error). Modern code usually uses async/await instead.
            • 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?

            CompleteFrontend Clear concepts. Working examples.