await under the hood
Learn how await wraps values, suspends async functions, resumes frames through promise jobs, and handles native promises and thenables.
- 01Trace an awaitExplain how an operand becomes a promise, suspends one async function, and later resumes or throws at the await expression.
- 02Read job orderingUse marker jobs to compare native promises, ordinary values, and thenables without relying on clock timing.
- 03Model 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.
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.
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.
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.
Replay an instrumented teaching model of await. It uses the lesson's real values; it is not an engine debugger.
script
async function serveOrder(order) { const food = await order; return "serving " + food;} 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.
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.
| Awaited input | PromiseResolve result | What the continuation waits for |
|---|---|---|
| Plain value | PromiseResolve makes a fulfilled promise | The async function resumes in a later job |
| Native promise | The native promise is used directly | Modern engines take the short resume path |
| Thenable | Its callable then must be adopted | A job calls user-provided then before resume |
| Rejected input | The rejection reaction is attached | The 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.
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.
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.
Replay a hand-written state-machine model. Engines do not literally use this JavaScript object, but async functions keep equivalent continuation state.
script
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"));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.
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.
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.
async function waitForScore() { const score = await 42; console.log("value", score);} waitForScore();Promise.resolve() .then(() => console.log("tick 1")) .then(() => console.log("tick 2"));Run a selection to see the ordered events.
Pick one input and run it. Changing the choice clears the previous output.
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.
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.
- Call
serve() - Log
next customer - Turn
42into a fulfilled promise - Remember
foodand the next line - Run a native promise reaction job
- Call a thenable's
thenin a job
Sort each card into the caller, await machinery, or later-resume stage.
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.
| Word | What it means | Do not confuse it with |
|---|---|---|
await | Pauses the current async function | Pausing the whole JavaScript app |
| PromiseResolve | Uses a native promise or wraps another value | Calling a thenable's then inline |
| Frame | Saved locals and continuation state | A browser animation frame |
| Tick marker | One promise job used for ordering evidence | A 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
A food counter starts an async order. Type the first printed line, before the caller reaches its own next line.
async function serve() {
console.log("order taken");
const food = await "burger";
console.log("serving", food);
}
serve();
console.log("next customer");order taken prints first. It is before the await, so it runs in the caller's current synchronous work.
Run the small value example mentally. What line contains the successful resumed value?
async function waitForScore() {
const score = await 42;
console.log("value", score);
}
waitForScore();
Promise.resolve()
.then(() => console.log("tick 1"))
.then(() => console.log("tick 2"));It eventually prints value 42. Await accepts ordinary values and resumes with that same value in a later job.
A checkout waits for tea and then for a burger. Which saved variable holds the first completed item?
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"));The saved field is drink. It stays in the frame while the second await is still pending.
A checkout SDK returns a thenable. Which method will the adoption job call to follow its eventual result?
The job calls the thenable's then method. That method is user code, which is why adoption has a job boundary.
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?
async function showOrder(order) {
const food = await order;
console.log("show", food);
}
showOrder(Promise.resolve("burger"));The rest of the screen should stay usable. Only the showOrder call is suspended while its order promise is pending.
You add `tick 1` and `tick 2` markers around an await. What are those markers evidence for?
The markers measure promise-job ordering. They make relative queue order visible without claiming a number of milliseconds.
Check your understanding
Use the three questions from the examples: what runs synchronously, what job gets queued, and what saved frame resumes later?
Question 1 of 7What does this snippet print first?
Read the code, then predictasync function serve() { console.log("order taken"); await "burger"; console.log("serving"); } serve(); console.log("next customer");Choose an answer to see the explanation.
Question 2 of 7What does await do to a plain value such as
42?Choose an answer to see the explanation.
Question 3 of 7What is saved while an async function is suspended?
Choose an answer to see the explanation.
Question 4 of 7Why is a thenable different from a native promise?
Choose an answer to see the explanation.
Question 5 of 7What do the tick markers prove?
Choose an answer to see the explanation.
Question 6 of 7If an awaited promise rejects, what happens at await?
Choose an answer to see the explanation.
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
thenis 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.