cf.completefrontendCode editorOpen lab
THE JAVASCRIPT FIELD GUIDE

await under the hood

Learn how await wraps values, suspends async functions, resumes frames through promise jobs, and handles native promises and thenables.

By the end, you can
  • 01
    Trace an awaitExplain how an operand becomes a promise, suspends one async function, and later resumes or throws at the await expression.
  • 02
    Read job orderingUse marker jobs to compare native promises, ordinary values, and thenables without relying on clock timing.
  • 03
    Model saved executionDescribe an async function as a resumable frame that preserves variables and its next step across awaits.

One function pauses; the app does not

await pauses one async function until a value is ready. It does not freeze the page, the event loop, or the function that called it. The paused function remembers its local variables and the next line it must run.

Definition

An await continuation is the later promise job that resumes a suspended async function with a value, or throws a rejection reason at the await expression.

An async function pauses at awaitPop out in the code editor (opens in a new tab)JavaScript
async function serve() {  console.log("order taken");  const food = await "burger";  console.log("serving", food);} serve();console.log("next customer");

Line 2 prints order taken. Line 3 reaches await "burger", so serve pauses. Line 7 is ordinary caller code, so it prints next customer. Later the continuation puts burger into food, and line 4 prints serving burger.

The real output is order taken, next customer, then serving burger. This is the same promise-job foundation from How promises work inside, but now the saved reaction resumes an async function rather than only calling a handler.

The await algorithm

The specification describes await as a short sequence of actions. First it evaluates the operand. Next it uses PromiseResolve: a native promise is used as it is, while a value or promise-like object is turned into a promise-shaped result that the rest of the algorithm can follow.

One value goes through the await pathPop out in the code editor (opens in a new tab)JavaScript
async function serveOrder(order) {  const food = await order;  return "serving " + food;} serveOrder("burger").then(console.log);

Line 2 evaluates order and starts the await operation. The algorithm attaches a resume reaction and a throw reaction to its promise result. The async function suspends and line 6 gets a promise back immediately. When the value is fulfilled, a later job resumes line 2 and line 3 returns serving burger.

Rejection uses the same saved place with a different completion. If the awaited promise rejects, the later job throws the reason at line 2. A try/catch surrounding that await can handle it just like a normal throw. Await therefore keeps familiar error control flow while still leaving the current call stack.

Step through one await
Step 0 of 6Ready
Your turn: follow the blue line

Replay an instrumented teaching model of await. It uses the lesson's real values; it is not an engine debugger.

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
async function serveOrder(order) {  const food = await order;  return "serving " + food;} 
CallStoreChangeResultRun = next line. 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.
Real-life analogyPausing a YouTube video

Pausing a YouTube video does not pause your whole phone. The player remembers the exact position, and pressing play continues from that point instead of starting over.

In real life: A video pauses at one moment
In JavaScript: One async function suspends at await
In real life: The player remembers the position
In JavaScript: The frame keeps locals and next step
In real life: Other phone apps still work
In JavaScript: Other JavaScript can keep running
In real life: Press play and continue there
In JavaScript: A job resumes at the saved await

Where the analogy stops: A video player is not a promise engine; it only explains saving one position and returning to it later.

Native promises and tick markers

A microtask tick in this lesson means one promise job that we intentionally log. It is not a duration in milliseconds. Markers let us see relative order without turning a busy computer into a timing experiment.

Await a ready native promisePop out in the code editor (opens in a new tab)JavaScript
const nativePromise = Promise.resolve("tea"); async function waitForTea() {  const drink = await nativePromise;  console.log("native", drink);} waitForTea();Promise.resolve()  .then(() => console.log("tick 1"))  .then(() => console.log("tick 2"))  .then(() => console.log("tick 3"));

Line 1 creates an already fulfilled native promise. Line 4 awaits it. Calling waitForTea reaches that await and returns to the caller. Lines 8 through 11 queue marker jobs. In current engines, the tested output starts with native tea then prints tick 1, tick 2, and tick 3.

That first output does not mean await became synchronous. It means its resume job was queued before the marker chain. The caller had already returned from waitForTea before any of those logs happened. Add markers when debugging a real ordering question; do not infer order from a wall-clock measurement.

How V8 removed extra ticks

Older async-function implementations did extra promise wrapping work when awaiting a native promise. V8 7.2 described its faster async functions work: a native-promise await that had cost three microtask ticks could use one. The shorter path was later adopted into the specification.

History is useful, but this page does not pretend that an old engine is installed in your browser. The runnable snippet above proves only today's observable order on the runtime that executes it. It is a concrete reason that the native-promise path is worth distinguishing from a thenable path, not a benchmark claim about every past engine.

Inputs that look similar at an await
Awaited inputPromiseResolve resultWhat the continuation waits for
Plain valuePromiseResolve makes a fulfilled promiseThe async function resumes in a later job
Native promiseThe native promise is used directlyModern engines take the short resume path
ThenableIts callable then must be adoptedA job calls user-provided then before resume
Rejected inputThe rejection reaction is attachedThe reason is thrown at the await expression

The important design result is modest: native promises can take a direct route because the engine knows their promise behavior. A custom object with a then method needs more careful adoption. The next two sections make that difference visible without requiring engine source code.

Async functions as resumable frames

A resumable frame is a useful model for the saved state of one async-function call. It includes values that will be used after an await and a continuation point: which part of the function should run when a job returns. Engines optimize and represent this in their own ways; the small object below is not engine source code.

A tiny saved-frame teaching modelPop out in the code editor (opens in a new tab)JavaScript
function makeServeFrame() {  return { step: 0, order: "burger", food: undefined };} function resumeServe(frame, value) {  if (frame.step === 0) {    frame.step = 1;    return "suspended";  }  frame.food = value;  frame.step = 2;  return "serving " + frame.food;} const frame = makeServeFrame();console.log(resumeServe(frame));console.log(resumeServe(frame, "burger"));

Line 2 creates a frame with step: 0. The first call on line 15 changes that step to 1 and returns suspended. The second call supplies burger, stores it in food, and returns serving burger. The real output has those two lines in that order.

This model makes one important fact visible: a later job does not restart the function at line 1. It resumes at the saved continuation. That is why a variable declared before await can still be used after it. It also explains why a local variable is not shared with a second, separate call to the async function.

Saving two waits

One async call can suspend more than once. Each resume moves the same activation forward. The frame keeps the first completed value while the second value is still pending, so a final line can combine both.

A two-await state-machine modelPop out in the code editor (opens in a new tab)JavaScript
function makeCheckoutFrame() {  return { step: 0, drink: undefined, food: undefined };} function resumeCheckout(frame, value) {  if (frame.step === 0) {    frame.step = 1;    return "wait for drink";  }  if (frame.step === 1) {    frame.drink = value;    frame.step = 2;    return "wait for food";  }  frame.food = value;  frame.step = 3;  return frame.drink + " and " + frame.food;} const frame = makeCheckoutFrame();console.log(resumeCheckout(frame));console.log(resumeCheckout(frame, "tea"));console.log(resumeCheckout(frame, "burger"));

Line 20 creates the frame. Line 21 reaches its first wait and prints wait for drink The next call stores tea in drink and prints wait for food The final call stores burger and prints tea and burger.

The step-through uses this same model with immutable snapshots. It is deliberately slower and more visible than an engine. Its value is that you can watch the saved variables and step change without guessing where a suspended function will continue.

Step through a saved async frame
Step 0 of 7Ready
Your turn: follow the blue line

Replay a hand-written state-machine model. Engines do not literally use this JavaScript object, but async functions keep equivalent continuation state.

Running in
  1. script
Next: line 20
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
function makeCheckoutFrame() {  return { step: 0, drink: undefined, food: undefined };} function resumeCheckout(frame, value) {  if (frame.step === 0) {    frame.step = 1;    return "wait for drink";  }  if (frame.step === 1) {    frame.drink = value;    frame.step = 2;    return "wait for food";  }  frame.food = value;  frame.step = 3;  return frame.drink + " and " + frame.food;} console.log(resumeCheckout(frame));console.log(resumeCheckout(frame, "tea"));console.log(resumeCheckout(frame, "burger"));
CallStoreChangeResultRun = next line. 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.

Awaiting ordinary values

await 42 is valid. It does not return 42 inline. PromiseResolve creates the fulfilled path, then the async function yields once so other already queued jobs can have a turn. This makes await behavior consistent whether a helper returns a value today or a promise tomorrow.

An ordinary value still yields oncePop out in the code editor (opens in a new tab)JavaScript
async function waitForScore() {  const score = await 42;  console.log("value", score);} waitForScore();Promise.resolve()  .then(() => console.log("tick 1"))  .then(() => console.log("tick 2"));

Line 2 awaits a number. Line 6 starts the function and line 7 adds a marker chain. The real output is value 42 then tick 1, then tick 2. The resume happened in a later job, but that job was queued before the marker chain.

This is helpful in app code. A cache function can return an already known product or a pending fetch promise, and a caller can always use await getProduct() The caller does not need two different control-flow shapes. It should still handle errors deliberately, because a rejected promise becomes a throw at the await.

Awaiting thenables

A thenable is an object or function with a callable then method. It is not required to be a native Promise Because that method is user-provided code, the promise machinery schedules a job to call it instead of simply treating it like a trusted native promise.

A thenable has an extra adoption boundaryPop out in the code editor (opens in a new tab)JavaScript
const thenable = {  then(resolve) {    console.log("thenable then");    resolve("tea");  },}; async function waitForTea() {  const drink = await thenable;  console.log("thenable", drink);} waitForTea();Promise.resolve()  .then(() => console.log("tick 1"))  .then(() => console.log("tick 2"))  .then(() => console.log("tick 3"));

Lines 1 through 6 define the thenable. Line 10 reaches its await. A later adoption job calls line 2, so the output first shows thenable then The marker job then prints tick 1 and the fulfilled continuation prints thenable tea before the remaining markers in the tested run.

The exact purpose of the extra boundary is safety and compatibility, not punishment. A thenable can come from a library, can throw, and can try to settle more than once. Promise resolution has rules for those cases. In day-to-day code, prefer native promises from APIs you own, and recognize thenables when a curious ordering difference appears.

Compare await inputs

Pick exactly one input: a value, a native promise, or a thenable. The playground prints the caller's synchronous line, two marker jobs, and the eventual resume. Reset restores the value selection and removes old output, so each comparison begins with an independent run.

Playground: choose one awaited input
The selected await probePop out in the code editor (opens in a new tab)JavaScript
async function waitForScore() {  const score = await 42;  console.log("value", score);} waitForScore();Promise.resolve()  .then(() => console.log("tick 1"))  .then(() => console.log("tick 2"));
Observed ordernot run

Run a selection to see the ordered events.

Try it yourself

Pick one input and run it. Changing the choice clears the previous output.

The program uses real JavaScript promises. The labels make job order visible; they are not millisecond timings.

Start with 42, then switch to a native promise, then choose the thenable. Look for one question rather than many: which jobs must happen before the suspended function can resume? The answer is clearer than trying to count invisible milliseconds.

What this changes in app code

You rarely need to write promise jobs yourself. The useful habit is to keep UI work small before an await, return promises to the owner that can handle failure, and remember that the rest of the screen can still receive work while one function is suspended.

Keep an order screen responsivePop out in the code editor (opens in a new tab)JavaScript
async function showOrder(order) {  console.log("loading");  const food = await order;  console.log("show", food);} showOrder(Promise.resolve("burger"));console.log("screen stays usable");

Line 2 prints loading before the wait. Line 3 pauses only showOrder Line 8 therefore prints screen stays usable before line 4 later prints show burger A real screen can render a loading state, accept unrelated input, or allow navigation while a request is pending.

This is not permission to ignore races. If a user starts two requests, each async call has its own frame and may resume in a different order. Give requests an owner, cancel stale work where an API supports it, or compare a current request ID before writing old data into the screen. The next lessons on microtasks and suspended frames add useful context.

Where does each await event belong?
  • Call serve()
  • Log next customer
  • Turn 42 into a fulfilled promise
  • Remember food and the next line
  • Run a native promise reaction job
  • Call a thenable's then in a job
Try it yourself
0 of 6 correct

Sort each card into the caller, await machinery, or later-resume stage.

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

Common misconceptions

  • “await freezes JavaScript.” It suspends only the current async function.
  • “await 42 is synchronous.” A plain value still resumes through a later promise job.
  • “A thenable is a Promise.” It only has a callable then; its method is user code.
  • “Tick count means elapsed time.” The markers reveal ordering, not milliseconds.
  • “A saved frame is a browser frame.” It is the function's continuation state.
Words that sound alike but mean different things
WordWhat it meansDo not confuse it with
awaitPauses the current async functionPausing the whole JavaScript app
PromiseResolveUses a native promise or wraps another valueCalling a thenable's then inline
FrameSaved locals and continuation stateA browser animation frame
Tick markerOne promise job used for ordering evidenceA millisecond measurement

When output is surprising, write the synchronous code first. Then write each queued promise job in order. Finally ask which one resumes the async function. This small trace works better than describing all delayed code as “callbacks,” because it names the exact boundary.

Practice exercises

Exercise 1 · Warm-upPredict the first log

A food counter starts an async order. Type the first printed line, before the caller reaches its own next line.

Starter codePop out in the code editor (opens in a new tab)JavaScript
async function serve() {
  console.log("order taken");
  const food = await "burger";
  console.log("serving", food);
}

serve();
console.log("next customer");

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

    Exercise 2 · Warm-upRead a non-promise await

    Run the small value example mentally. What line contains the successful resumed value?

    Starter codePop out in the code editor (opens in a new tab)JavaScript
    async function waitForScore() {
      const score = await 42;
      console.log("value", score);
    }
    
    waitForScore();
    Promise.resolve()
      .then(() => console.log("tick 1"))
      .then(() => console.log("tick 2"));

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

      Exercise 3 · PracticeKeep the first result

      A checkout waits for tea and then for a burger. Which saved variable holds the first completed item?

      Starter codePop out in the code editor (opens in a new tab)JavaScript
      function makeCheckoutFrame() {
        return { step: 0, drink: undefined, food: undefined };
      }
      
      function resumeCheckout(frame, value) {
        if (frame.step === 0) {
          frame.step = 1;
          return "wait for drink";
        }
        if (frame.step === 1) {
          frame.drink = value;
          frame.step = 2;
          return "wait for food";
        }
        frame.food = value;
        frame.step = 3;
        return frame.drink + " and " + frame.food;
      }
      
      const frame = makeCheckoutFrame();
      console.log(resumeCheckout(frame));
      console.log(resumeCheckout(frame, "tea"));
      console.log(resumeCheckout(frame, "burger"));

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

        Exercise 4 · PracticeName thenable adoption work

        A checkout SDK returns a thenable. Which method will the adoption job call to follow its eventual result?

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

          Exercise 5 · ChallengeApply await to a product screen

          A product request is pending after the user pressed Add to cart. What should remain usable in the app while this one async function awaits?

          Starter codePop out in the code editor (opens in a new tab)JavaScript
          async function showOrder(order) {
            const food = await order;
            console.log("show", food);
          }
          
          showOrder(Promise.resolve("burger"));

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

            Exercise 6 · ChallengeUse markers honestly

            You add `tick 1` and `tick 2` markers around an await. What are those markers evidence for?

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

              Check your understanding

              Use the three questions from the examples: what runs synchronously, what job gets queued, and what saved frame resumes later?

              await internals quiz · 7 questionsScore: first tries count
              1. Question 1 of 7What does this snippet print first?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                async function serve() {
                  console.log("order taken");
                  await "burger";
                  console.log("serving");
                }
                serve();
                console.log("next customer");

                Choose an answer to see the explanation.

              2. Question 2 of 7What does await do to a plain value such as 42?

                Choose an answer to see the explanation.

              3. Question 3 of 7What is saved while an async function is suspended?

                Choose an answer to see the explanation.

              4. Question 4 of 7Why is a thenable different from a native promise?

                Choose an answer to see the explanation.

              5. Question 5 of 7What do the tick markers prove?

                Choose an answer to see the explanation.

              6. Question 6 of 7If an awaited promise rejects, what happens at await?

                Choose an answer to see the explanation.

              7. Question 7 of 7Which statement best describes modern native-promise await?

                Choose an answer to see the explanation.

              Key takeaways

              • Await evaluates its operand, uses PromiseResolve, attaches resume and throw reactions, then suspends one async function.
              • The caller gets control back while the suspended function waits for a later promise job.
              • A native promise uses a short modern path; a thenable needs adoption work because its then is user code.
              • Async functions act like resumable frames that retain local values and the next continuation point.
              • Awaiting 42 still yields once, so code can use one async shape for values and promises.
              • Tick markers show ordering in a specific snippet, not speed in milliseconds.

              Remember the one-liner.
              Await pauses one async function, saves its frame, and resumes it later through promise machinery.

              Coming next: The microtask queue, where those promise jobs wait before a host runs them.

              CompleteFrontend Clear concepts. Working examples.