cf.completefrontendCode editorOpen lab
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
    Hold a two-way conversationPredict how next(value), throw(error), and return(value) resume a paused yield.
  • 02
    Model async control flowStep through a tiny promise runner and compare the same sequence with async/await.
  • 03
    Coordinate and clean upUse finally for early exits and reason about cooperative coroutines without preemption.

Generators can pause more than iteration

You already know that a generator can produce values over time. This lesson uses the same pause-and-resume power for control flow: deciding who runs next, what value resumes a paused line, and what cleanup happens when the caller stops early.

A generator function, written with function*, does not run immediately. Calling it creates an iterator. Each next() resumes the body until the next yield, then returns an iterator result. The advanced part is that the caller can send a value back into the paused yield.

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

Imagine a walkie-talkie exchange: “What is your name?” Then silence. The other person replies, “Ada.” The first person continues from exactly that point using the answer. A generator conversation works the same way, except the pause 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 uses next(answer)
In real life: The reply is heard where the first person paused
In JavaScript: That answer becomes the value of the paused yield expression

Where the analogy stops: A walkie-talkie has humans choosing words. A generator follows exact code, and the first next(value) has nowhere to deliver its value yet, so that first argument is ignored.

We will build from a tiny conversation to a promise runner, then use early exits, coroutines, and a state machine. The goal is not to replace async/await. It is to see why paused programs are powerful.

Sending values with next()

STEP THROUGH

The expression yield "question" does two jobs. First it sends "question" out to the caller. Later, when the caller resumes with next(answer), the whole yield expression evaluates to answer inside the generator.

The first call is special: it starts the body. Because the generator is not paused at a yield yet, the first next("ignored") argument is ignored. Step through the replay and switch the answers to prove it.

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

Predict each printed value. Step through how next(value) is a two-way conversation with the paused yield.

Running in
  1. script
Next: line 6
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
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);
CallStoreChangeResultRun = next line. Ran = already executed.
Recent returnsNothing yet. Start with the blue line.
Change the answers sent by lines 8 and 9.

Changing the answers starts a fresh replay. Notice that line 7's argument is still ignored.

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 precise rule

The value passed to next(v) becomes the result of the yield expression where the generator is currently paused. If the generator has not reached a yield yet, there is no expression to receive v.

The same idea powers throw(error) and return(value). throw(error) raises an error at the paused yield, where a try/catch inside the generator can handle it. return(value) asks the generator to finish, but it still runs finally blocks on the way out.

Generators + promises

INTERACTIVE

Before async/await became the standard way to write promise workflows, libraries such as co used generators to make asynchronous code read top to bottom. The generator yielded a promise. A runner waited for it. When it fulfilled, the runner called next(result); when it rejected, the runner called throw(error).

Real-life analogyA promise runner is a personal assistant

Picture a personal assistant: you pause your plan and hand over an errand. The assistant leaves, waits, and returns with either the result or the problem. Then you continue the plan from exactly 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 fulfillment or rejection
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)

Where the analogy stops: This explains the control flow shape, not engine implementation. Modern async/await is specified directly by JavaScript, even though the model feels similar.

Promise runner lab
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";}
Same control flow 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";}
Runner timelinesuccess
final resultClick Run
Try it yourself

The runner waits for each yielded promise, then sends the fulfilled value back with next(value).

This is a tiny version of the pattern libraries like co popularized before async/await. Modern JavaScript specifies async/await directly.

The modern version is shorter because the language owns the runner. In everyday app code, prefer async & await. The generator runner is still worth learning because it reveals the handoff: pause, wait, send the value back, continue.

Cleanup with finally

INTERACTIVE

A generator may hold a resource: a file-like reader, a lock, a subscription, or just a piece of state that must be cleaned up. Put cleanup in finally. If the caller stops early with return(), or a for...of loop breaks, the generator gets a chance to run that finally block.

Cleanup lab: early exits run finally
Resource-holding generatorPop 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");  }}
Observed outputreturn

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() begins cleanup. A yield inside finally can make cleanup pause again, so avoid yielding there in real resource cleanup.

The source intentionally includes a yield inside finally to show the edge case. The lesson's advice is to avoid that pattern for real cleanup.
Avoid yielding inside cleanup

The lab deliberately has a yield inside finally. That is legal, and it proves cleanup can pause again. It is also a trap: the resource may not be fully closed until the generator is resumed. Real cleanup code should usually finish without yielding.

This works with the same error handling ideas from try, catch & finally. The difference is that generators let the caller request an early exit through the iterator protocol.

Coroutines: taking turns on purpose

INTERACTIVE

A coroutine is a routine that can cooperate with other routines by choosing when to pause. With generators, each task runs until it reaches yield. A scheduler can then resume a different task.

Real-life analogyCoroutines are chess players taking turns

In chess, a player gets a turn, decides, moves, and only then does the other player act. Coroutines feel like that: each unit of work hands control back voluntarily.

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 resumes another generator
In real life: No one grabs a piece mid-move
In JavaScript: No task is interrupted halfway through synchronous work

Where the analogy stops: Real applications also have browser tasks, microtasks, rendering, workers, and operating-system scheduling. This lesson's scheduler is cooperative and single-threaded.

Coroutine lab: cooperative turns
Round-robin schedulerPop 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);}
Generator 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);
Live timeline0/7

Click Run next turn to start.

Traffic light state machinegreen → yellow → red → green → yellow → red
Step 0 of 7cooperative turns

Each task runs until its own yield, then the scheduler puts it at the back of the queue. No task is interrupted in the middle of a move.

This is a cooperative scheduler: useful as a mental model and for small orchestrations, not a replacement for browser scheduling APIs.

This is not parallelism. It is a clear way to model turn-taking work, animation scripts, parsers, or teaching tools where pausing at named points is more useful than running everything at once.

Generators as state machines

A state machine moves through known states: green, yellow, red, then green again; or a game dialog that waits at each prompt. A generator makes that style readable because each yield is a named state, and each next() advances one step.

Generator runner vs async function
QuestionGenerator control flowAsync function
Pause keywordyield pauses a generatorawait pauses an async function
Who resumes itThe caller uses next, throw, or returnThe promise machinery resumes it when the awaited value settles
Result shapeIterator results: { value, done }A promise that fulfills or rejects
Best modern useIteration, custom control flow, teaching, cooperative schedulingProduction promise workflows

Use this comparison to choose tools honestly. Generators are excellent for custom iteration, controlled replay, cooperative tasks, and little state machines. Promise workflows usually belong in async functions now.

Where you'll use this

SORTER

Generator control flow appears in libraries, test utilities, parsers, animation experiments, and interview questions. The practical skill is not “rewrite everything as a generator.” It is knowing when a pause point helps the code tell the truth.

Which function shape fits the job?
  • A wizard asks one question, waits for an answer, then asks the next
  • Load JSON with await fetch(...) in production app code
  • Turn cents into a price string immediately
  • A toy scheduler lets tasks politely take turns
  • Try a request, catch errors, and await a delay before retrying
  • Add all numbers in an array and return the total
Try it yourself
0 of 6 correct

Sort each task by the simplest control-flow tool that fits it. The explanations call out when generators are useful and when they are overkill.

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 cannot. The body has not reached a yield yet, so the first argument is ignored. The second call is the first one that can feed a paused expression.

“yield and await are the same thing.”

They both pause a function-like body, but they are different language features. yield talks to an iterator caller; await works inside async functions and resumes from promise settlement.

“A coroutine scheduler interrupts tasks whenever it wants.”

Not with generators. A task runs until it voluntarily reaches yield. Long synchronous work still blocks everything else.

“return() skips finally.”

return() asks a generator to finish, but finally blocks still run. That is why they are the right home for cleanup.

“The generator runner is how JavaScript engines implement await.”

No. It is an accurate model of the control flow, and libraries used it before async/await, but the language specifies async functions directly.

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 proves 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) for fulfilled promises. What 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 round robin

        What order does the scheduler print?

        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 boring tool

          For a real profile loader that awaits 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

            Answer once from prediction, then read every explanation. The wrong answers are designed around common generator mistakes.

            Lesson quiz · 7 questionsScore: first tries count
            1. Question 1 of 7What happens to the argument passed to 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 7How does it.throw(error) behave for 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 coroutines in this lesson's scheduler?

              Choose an answer to see the explanation.

            7. Question 7 of 7Which tool should you usually choose for everyday promise workflows now?

              Choose an answer to see the explanation.

            Key takeaways

            • next(value) sends value into the paused yield expression; the very first argument is ignored.
            • throw(error) raises an error at the paused yield; return(value) finishes while still running finally blocks.
            • A generator promise runner models “wait, resume with the result, or throw the error back,” but modern code usually uses async/await.
            • Coroutines are cooperative: each task runs until it chooses to yield.
            • Generators are useful for custom iteration, controlled replay, state machines, and small schedulers—not for every function.

            Remember the one-liner.
            A generator is a paused program the caller can resume with a value, an error, or an early finish.

            Up next: Why modules?

            CompleteFrontend Clear concepts. Working examples.