completefrontend
Open lab My progress
THE JAVASCRIPT FIELD GUIDE

How async code is compiled down

Learn how transpilers turn generators and async functions into resumable state machines, runners, and compatibility code for older browsers.

By the end, you can
  • 01
    Read a state machineMatch each yield in a generator to a branch of a small switch that saves the next step.
  • 02
    Explain rewritten asyncSay how a generator plus a promise runner keeps an async function's order.
  • 03
    Choose a targetSpot when older output adds helper code, extra work, and harder debugging.

What compiled down means

Modern JavaScript engines understand async, await, and generators. Very old browsers do not. A transpiler is a tool that rewrites new JavaScript into older JavaScript. The goal is that the program still does the same things, in the same order.

Definition

Compiled down means rewritten into older JavaScript that an older browser can run. For async code, each pause becomes ordinary code: a saved step number, plus helper code that starts the work again later.

The rewrite cannot skip the waiting. Instead, it writes out each pause in plain code. Each pause needs three things:

  • a place to keep the values the function still needs;
  • a note that says which step comes next;
  • a promise callback that starts the work again when the result arrives. A callback is a function you hand over so other code can call it later.

This lesson follows on from the microtask queue. That lesson showed where promise jobs wait. This lesson shows the extra JavaScript a build tool can write to make pauses work in older browsers.

A generator pauses at yield

Start with a generator, because it shows pausing clearly. A generator is a function that can stop in the middle and carry on later. You write it with a star after the word function: function*. Inside it, yield marks a pause point. There, the function hands back a value and stops.

Calling a generator does not run its body. It gives you a generator object. Each time you call the object's next() method, the body runs up to the next yield and stops again.

A generator with three tea stepsPop out in the code editor (opens in a new tab)JavaScript
function* makeTea() {  yield "boil water";  yield "add tea";  yield "pour";} const tea = makeTea();console.log(tea.next().value);console.log(tea.next().value);console.log(tea.next().value);

Line by line:

  • Line 1 starts the generator makeTea.
  • Lines 2, 3, and 4 each have a yield. Each one is a pause point with one tea step.
  • Line 7 calls makeTea(). This makes one generator object, tea. No step runs yet.
  • Lines 8, 9, and 10 each call tea.next(). The console prints boil water, add tea, and pour, in that order.
The console prints boil water, then add tea, then pour. Line 8 is the first next call, line 9 the second, and line 10 the third.Consoleboil wateradd teapourline 8: first next()line 9: second next()line 10: third next()
Each next() runs the generator up to the next yield, then it pauses again. Three calls give the three steps, in order.

Each call to next() returns a small object, such as { value: "boil water", done: false }. It has two parts:

  • value is what the yield handed back.
  • done says whether the generator has finished. It stays false until there are no steps left.

Lines 8 to 10 print only the value part.

Step through a generator
Step 0 of 7Ready
Your turn: follow the blue line

Step through a real generator run. This records the lesson's own example step by step; it is not the engine's debugger.

Running in
  1. script
Next: line 7
function* makeTea() {  yield "boil water";  yield "add tea";  yield "pour";} console.log(tea.next().value);console.log(tea.next().value);console.log(tea.next().value);
CallStoreChangeResultBlue line: runs next. Ran: already executed.
Recent returnsNothing yet. Start with the blue line.
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.

Generators as state machines

A state machine is ordinary code that remembers which step it is on. Each time you call it, it looks at the saved step, does that step, and saves the next one. This is a good way to picture a generator, because a generator also has to remember which yield it stopped at.

The same tea steps as a state machineJavaScript
function makeTeaMachine() {  let state = 0;  return {    next() {      switch (state) {        case 0: state = 1; return { value: "boil water", done: false };        case 1: state = 2; return { value: "add tea", done: false };        case 2: state = 3; return { value: "pour", done: false };        default: return { value: undefined, done: true };      }    },  };}

Here state is a plain number. A switch picks one branch, one case, by that number. Line by line:

  • Line 2 sets state to 0, so the first call starts at the first step.
  • The first next() takes line 6. It sets state to 1 and returns boil water.
  • The second call takes line 7. It sets state to 2 and returns add tea.
  • The third call takes line 8. It sets state to 3 and returns pour.
  • A fourth call finds no case for 3. It takes the default branch on line 9 and returns done: true.
The first next call. Before the call, the box called state holds 0. The call returns boil water. After the call, the state box holds 1.0statebefore the callnext()boil waterreturned1stateafter the call
The first call reads state, which is 0, so it runs the step for 0 and returns boil water. Before it returns, it writes 1 into state, so the next call runs the next step.
Real-life analogyA recipe with a sticky note

Think of a recipe with numbered steps. A sticky note on the page says where you stopped. Each time you come back, you read the note, do that step, and move the note to the next step. The state machine is the recipe plus that note.

In real life: A recipe has numbered steps
In JavaScript: The function body has pause points
In real life: A sticky note says step 3
In JavaScript: The saved state number says where to carry on
In real life: You come back to the recipe later
In JavaScript: A later next() call carries on
In real life: You move the note after each step
In JavaScript: Each branch saves the next state

This machine is a model for learning. It is not the code inside a real engine; engines store paused functions in their own, faster way. The main point is simple: a function that can pause must save where it stopped. A saved number plus a switch is one way to do that in ordinary JavaScript.

Step through the matching state machine
Step 0 of 10Ready
Your turn: follow the blue line

Step through the hand-written state machine. It gives the same results as the generator with three yields.

Running in
  1. script
Next: line 2
function makeTeaMachine() {  return {    next() {      switch (state) {        case 0: state = 1; return { value: "boil water", done: false };        case 1: state = 2; return { value: "add tea", done: false };        case 2: state = 3; return { value: "pour", done: false };        default: return { value: undefined, done: true };      }    },  };}
CallStoreChangeResultBlue line: runs next. Ran: already executed.
Recent returnsNothing yet. Start with the blue line.
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.

Compare the generator and the machine

Inside, the two versions look different. From the outside, they act the same: each call gives the next tea step. The machine also shows one thing a real generator keeps hidden: its saved state number.

Playground: call next() on both versions
The generatorPop out in the code editor (opens in a new tab)JavaScript
function* makeTea() {  yield "boil water";  yield "add tea";  yield "pour";} const tea = makeTea();console.log(tea.next().value);console.log(tea.next().value);console.log(tea.next().value);
The state machineJavaScript
function makeTeaMachine() {  let state = 0;  return {    next() {      switch (state) {        case 0: state = 1; return { value: "boil water", done: false };        case 1: state = 2; return { value: "add tea", done: false };        case 2: state = 3; return { value: "pour", done: false };        default: return { value: undefined, done: true };      }    },  };}
What each call returnedSaved state
call 1state 1

Generator: {"value":"boil water","done":false}

Machine: {"value":"boil water","done":false}

Step 1 of 4paused

After call 1, both versions return "boil water", and the machine saves state 1.

Both are real functions from this lesson. The machine is a model for learning, not real engine code. Reset goes back to 1 call.

Try these:

  • 1 call: both return boil water, and the machine saves state 1.
  • 3 calls: both reach pour.
  • 4 calls: both return done: true, because no steps are left.
  • Reset goes back to 1 call.

The table below compares four kinds of code that can pause. Read each row as: what it is, who starts it again, and what you get back.

Four kinds of code that can pause and carry on
What it isWho starts it again?What you get back
A generator.You do, by calling next().The value its yield hands back, and a done flag.
A state machine.An ordinary next() method reads a saved step number.The same value, and a new saved step number for next time.
An async function.Promises do, when the promise it waits for is ready.A promise for its final return value.
Older async output.A runner, written by the build tool, drives a rewritten generator or state machine.The same order as the original async function, with extra helper code.

A build tool can write its output in different shapes. But every shape must keep the same three facts:

  • which step runs next;
  • the local variables (variables made inside the function) that later steps still read;
  • how to return the next result without running earlier steps again.

Regenerator and async-to-generator

Regenerator is a tool used by Babel, a popular transpiler. Regenerator rewrites a generator into ordinary code. It works like the tea machine: a switch reads a saved step number, often called context.next. It also saves the values that later steps still need.

A short sketch of Regenerator-style outputJavaScript
// Simplified sketch of Regenerator output. It does not run on its own.function makeTea(context) {  while (1) switch (context.next) {    case 0: context.next = 2; return "boil water";    case 2: context.next = 4; return "add tea";    case 4: return context.stop("pour");  }}

This sketch is cut down on purpose, and it does not run on its own. Line by line:

  • Line 2 starts the rewritten makeTea. It gets a context object that holds the saved step number.
  • Line 3 reads the saved step number, context.next, and picks a branch.
  • Lines 4 and 5 each save the following step number and return one tea step.
  • Line 6 returns the last step, pour, and marks the generator as finished.

Real output does more. It also handles errors, an early return, throw, and loops.

Async-to-generator is the next step. It rewrites an async function into a generator. Each await becomes a yield that hands out a promise. A small helper, called a runner, drives the generator. When a promise is ready, the runner starts the generator again and gives it the promise's value.

The original async functionPop out in the code editor (opens in a new tab)JavaScript
async function makeOrder() {  console.log("start order");  const item = await Promise.resolve("tea");  console.log("serve " + item);} makeOrder();

Line by line:

  • Line 2 prints start order before the pause.
  • Line 3 waits for a promise that holds "tea".
  • When that promise is ready, line 4 prints serve tea.
  • Line 7 calls the function.

The next section builds a small runner that keeps this same order, using a generator.

A tiny async-to-generator runner

The runner can be small, because a promise already knows how to call code later. The runner does three jobs:

  • It starts the generator.
  • It checks whether the generator has finished.
  • If not, it waits for the promise the generator handed out. Then it calls next(value) with that promise's value.
A small runner that drives a generatorPop out in the code editor (opens in a new tab)JavaScript
function run(genFn) {  const iterator = genFn();  function advance(method, value) {    let step;    try {      step = iterator[method](value);    } catch (error) {      return Promise.reject(error);    }    if (step.done) return Promise.resolve(step.value);    return Promise.resolve(step.value).then(      value => advance("next", value),      error => advance("throw", error),    );  }  return advance("next");} function* makeOrder() {  console.log("start order");  const item = yield Promise.resolve("tea");  console.log("serve " + item);} run(makeOrder);

Line by line:

  • Line 2 calls the generator function. This gives the generator object, iterator.
  • Line 3 makes advance, the function that moves the generator one step.
  • Line 6 starts the generator again, with either next or throw.
  • Line 10: when the generator has finished, the runner finishes its own promise with the final value.
  • Lines 11 to 14: when the promise is ready, its value goes back in through next(value). If the promise fails, the error goes back in through throw(error). That way, a try/catch around the original await still works.
  • Line 16 makes the first move, with next.
  • Lines 19 to 22 are the async function, rewritten as a generator. Line 21 hands out the promise. On a later call, item gets "tea".
  • Line 25 starts everything.
Two function cards, makeOrder and run. An arrow from makeOrder to run: 1, yields a promise. An arrow back from run to makeOrder: 2, when the promise is ready, run calls next with tea.makeOrderthe generatorrunthe runner1. yields a promise2. when it is readynext("tea")
The generator hands the runner a promise and pauses. When the promise is ready, the runner calls next("tea"), and the generator goes on with item set to "tea".

The real async function and this runner print the same two lines, in the same order: start order, then serve tea. The lesson's tests run both programs with real promises to check this.

Coroutines keep their local state

A coroutine is a function that can pause and carry on later. While it is paused, it keeps its local variables. The word names what the function does, not a kind of syntax. Both of these are coroutines:

  • A generator. You drive it by calling next().
  • An async function. Promises drive it: it carries on when the promise it waits for is ready.
A generator keeps its own order numberPop out in the code editor (opens in a new tab)JavaScript
function* countOrders() {  let order = 1;  while (order <= 3) {    yield "order " + order;    order += 1;  }} const orders = countOrders();console.log(orders.next().value);console.log(orders.next().value);

Line by line:

  • Line 2 makes the local variable order and sets it to 1. This happens only once.
  • Line 9 makes the generator object, orders.
  • Line 10 calls next(). Line 4 hands back order 1 and pauses, and line 10 prints it.
  • Line 11 calls next() again. The generator carries on right after the yield.
  • Line 5 adds 1 to the same order variable. The loop goes round, line 4 hands back order 2, and line 11 prints it.

The generator never went back to line 1, so order was not set back to 1.

This is why rewritten code must save local variables. In the async example, item gets its value after the pause. The rewritten code stores enough to make the later branch act as if the function had never stopped.

The cost of older async output

Rewriting for older browsers solves one problem: old browsers can run the code. But it adds code. One short async function can turn into several pieces:

  • a generator body, or a switch like the tea machine;
  • a runner;
  • promise callbacks and saved local variables;
  • sometimes a shared helper library, called the Regenerator runtime. It goes into the bundle: the JavaScript file your site sends to the browser.
A short async function, and what older output can addJavaScript
async function saveOrder(order) {  const receipt = await send(order);  return receipt.id;} // Older targets can add a runner, a generator body,// saved state, and sometimes a shared helper library.

Line by line:

  • Line 1 starts a short async function.
  • Line 2 pauses until send(order) is ready, and saves the result in receipt.
  • Line 3 reads receipt after the pause. So older output must keep receipt safe across the pause.
  • The comments on lines 6 and 7 name the extra pieces. Different tools write different code, so the sketch does not show exact output.
Two windows. Modern output holds one card, async saveOrder. Older output holds four cards: generator body, runner, saved state, and helper library.Modern outputasync saveOrderyour code, as writtenOlder outputgenerator bodyrunnersaved statehelper library
A modern target keeps your function as you wrote it. An older target can need four pieces instead, and the helper library is only added sometimes.

The table below lists four kinds of cost. Read each row as: what it affects, what can change, and what you can do. It gives no fixed numbers, because the real cost depends on your code and your browsers.

What older async output can cost
What it affectsWhat can changeWhat you can do
File size.Helper code, and sometimes a shared helper library, go into the bundle.Build for modern browsers when your users allow it.
Work while running.Each pause can add promise callbacks, function calls, and objects.Measure on your own browsers before you claim a number.
Debugging.The rewritten switch steps do not look like the code you wrote.Use source maps, and look at the output your build really makes.
Which browsers can run it.Older browsers can run code they could not run before.Weigh the users you gain against the extra code.

Each pause can add extra function calls and extra objects. Debugging gets harder, too: one line you wrote, like line 2, can turn into several places in the rewritten code. A source map is a file that links the rewritten code back to the lines you wrote. With it, developer tools can show your own code.

Choosing which browsers to build for

Do not rewrite async code out of habit. First, decide where your product must run:

  • which browsers;
  • which web views (a small browser built into an app);
  • which runtimes (programs that run JavaScript outside a browser, such as Node.js).

This list is your target. All modern browsers run async and await as they are. So a modern target can keep the clear code you wrote.

Some projects must support older browsers. Then the extra helper code is a fair price, because it lets those users run the product. The important part is to choose the target on purpose. Then check the output and the bundle size yourself, instead of guessing.

  • Set your browser targets before you compare bundle output.
  • Look at the rewritten code when a stack trace (the list of function calls shown with an error) looks strange.
  • Keep source maps for debugging, both while you build and, if your team allows it, on the live site.
  • Read transpilers and polyfills for how to make the support decision.
  • Use native async wherever all your users' browsers can run it.

This does not mean modern output is always right for every site. It means helper code should be there because real users need it. Its size and its debugging cost should be part of that choice.

Sort the async pieces
  • yield "add tea" pauses and hands back a value.
  • A saved state picks a switch branch.
  • Promise.resolve(step.value).then(advance) starts the work again.
  • The promise's value becomes the result of a paused yield.
  • The browsers run async and await as they are.
  • An older target may add helper code and a helper library.
Try it yourself
0 of 6 correct

Put each card under the part that does that job in the tea examples.

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

Common misconceptions

  • “A generator runs in parallel, at the same time as other code.” It runs only when someone calls next(), and it stops at each yield.
  • “A state machine is fake async.” It is a real way to write a pause, and the later restart, in ordinary code.
  • “Every transpiler outputs Regenerator code.” The output depends on the tool, its settings, and the target.
  • “Older output always costs the same fixed amount.” The code and its speed vary, so measure on your own target.
  • “Async functions lose their local variables while they wait.” The paused function keeps everything that later lines need.

The table below lists words that are easy to mix up. Read each row as: the word, what it means, and the wrong idea people often have about it.

Words in this lesson that are easy to mix up
WordWhat it meansWhat it does not mean
State machine.A saved step number, plus branches that pick where to carry on.A special engine feature that only build tools can make.
Regenerator.A Babel tool, plus a helper library, that rewrites generators.The exact code that every JavaScript build tool writes.
Coroutine.A function that can pause and carry on, and keeps its local variables.A function that runs on another thread, at the same time as other code.
Native async.Modern engines run async and await as they are, with no rewrite.Always slower than rewritten code, or always exactly the same speed.

Keep the picture simple. A transpiler can rewrite pauses into step numbers, saved values, and callbacks. The program still follows the promise and microtask rules from the last lesson. (A microtask is a small job that runs as soon as the current code finishes.) Rewritten code does not get a secret clock of its own.

Practice exercises

Exercise 1 · Warm-upPredict the first tea step

Read the small generator. Type what the console prints.

Starter codePop out in the code editor (opens in a new tab)JavaScript
function* makeTea() {
  yield "boil water";
  yield "add tea";
}
const tea = makeTea();
console.log(tea.next().value);

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

    Exercise 2 · Warm-upRead the saved state

    After one call to the tea machine, what state number does it report?

    Starter codePop out in the code editor (opens in a new tab)JavaScript
    const machine = makeTeaMachine();
    console.log(machine.next().value);
    console.log(machine.getState());

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

      Exercise 3 · PracticePredict the runner order

      Follow this runner program in your head. Type the two printed lines, in order.

      Starter codePop out in the code editor (opens in a new tab)JavaScript
      function run(genFn) {
        const iterator = genFn();
        function advance(value) {
          const step = iterator.next(value);
          return step.done ? Promise.resolve(step.value) : Promise.resolve(step.value).then(advance);
        }
        return advance();
      }
      function* work() {
        console.log("start");
        const item = yield Promise.resolve("tea");
        console.log("finish " + item);
      }
      run(work);

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

        Exercise 4 · PracticeWho starts it again?

        A generator can stand in for an async function. Which piece starts that generator again after each pause?

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

          Exercise 5 · ChallengeChoose a target for a real site

          Your team is building a dashboard. It must run in modern desktop browsers and in an older web view inside an app. What should you check before you force older async output everywhere?

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

            Exercise 6 · ChallengeMake rewritten code readable again

            An error on your live site shows a stack trace that points into rewritten helper code. What tool helps you find the matching line in your original async function?

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

              Check your understanding

              Use the tea examples to keep three questions apart: what pauses, who starts it again, and what older output has to add. For code questions, go one pause at a time instead of guessing.

              How async compiles down quiz · 7 questionsScore: first tries count
              1. Question 1 of 7What does yield do in the tea generator?

                Choose an answer to see the explanation.

              2. Question 2 of 7What does this print?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                function* tea() {
                  yield "pour";
                }
                console.log(tea().next().value);

                Choose an answer to see the explanation.

              3. Question 3 of 7Why does the hand-written machine keep state?

                Choose an answer to see the explanation.

              4. Question 4 of 7A generator hands an async-to-generator runner a promise. What does the runner do when that promise is ready?

                Choose an answer to see the explanation.

              5. Question 5 of 7Which statement describes a coroutine in this lesson?

                Choose an answer to see the explanation.

              6. Question 6 of 7What is a likely cost of rewriting async code for older browsers?

                Choose an answer to see the explanation.

              7. Question 7 of 7When can a project usually skip the async helper code?

                Choose an answer to see the explanation.

              Key takeaways

              • A generator pauses at each yield and carries on when someone calls next().
              • A state machine can do the same job with a saved step number and a switch.
              • Regenerator-style output turns generator pauses into step numbers and saved local variables.
              • Async-to-generator uses a promise runner. The runner starts the generator again each time a promise is ready.
              • Coroutines pause and carry on while keeping their local variables. Generators and async functions differ in who starts them again.
              • Older async output can add bundle code, calls, objects, and debugging work. Choose a modern target when your users allow it.

              Remember the one-liner.
              A transpiler turns each pause into a saved step, plus code that knows how to carry on from it.

              Coming next: Writing engine-friendly code. You will learn simple code habits that help the engine run your code fast.

              CompleteFrontend Clear concepts. Working examples.