await under the hood
Learn how await wraps values, suspends async functions, resumes frames through promise jobs, and handles native promises and thenables.
- 01Follow one awaitSay how
awaitputs a value into a promise, pauses one function, and later starts it again or throws the error at theawaitline. - 02Read the order of jobsUse small marker jobs, not a clock, to compare awaiting a plain value, a native promise, and a thenable.
- 03Picture a saved functionDescribe a paused
asyncfunction as a saved frame that keeps its variables and its next step between waits.
One function pauses; the rest of the app keeps going
An async function is a function that can pause. Inside it, the word await means "wait here until this value is ready." Only that one function waits. The page does not freeze. The code that called the function keeps running.
While the function waits, it remembers two things:
- its local variables, with their values;
- the next line it must run.
When await pauses a function, JavaScript saves a small job to run later. This saved job is the await continuation. When the value is ready, the job starts the function again with that value. If the value is a promise that failed, the job throws the error at the await line instead.
async function serve() { console.log("order taken"); const food = await "burger"; console.log("serving", food);} serve();console.log("next customer");Line 7 calls serve. Line 2 prints order taken. Line 3 reaches await "burger", so serve pauses there. JavaScript goes back to the caller.
Line 8 is the caller's next line, so it prints next customer. Later, the saved job starts serve again. It puts "burger" into food, and line 4 prints serving burger.
serve pauses at await. The caller does not wait, so line 8 prints before line 4.A promise job is a small piece of work that JavaScript runs after the code that is running now has finished. You met promise jobs in How promises work inside. There, a saved job called a then handler. Here, the saved job starts a paused async function again.
The steps await follows
The specification is the official rulebook for JavaScript. It describes await as a short list of steps:
- Work out the value after
await. This value is called the operand. - Turn the operand into a promise. The spec calls this step PromiseResolve.
- Attach two reactions to that promise. One starts the function again with the value. The other throws the error.
- Pause the function, and give the caller a promise right away.
- Later, a promise job runs one of the two reactions.
Step 2 depends on the operand. A native promise is a promise made by JavaScript's own Promise, for example with Promise.resolve(). A native promise is used as it is. Any other value is first put into a new promise.
async function serveOrder(order) { const food = await order; return "serving " + food;} serveOrder("burger").then(console.log);Line 6 calls serveOrder("burger"). Line 2 works out order, which is the string "burger". The string is put into a promise, and the two reactions are attached. serveOrder pauses, and line 6 gets a promise back at once.
Later, a job starts the function again with "burger". Line 3 returns serving burger. That fills the promise that line 6 got, so .then(console.log) prints it.
A failed promise uses the same saved place. A promise that failed is called rejected, and the error it holds is called the reason. If the awaited promise is rejected, the later job throws the reason at line 2. A try/catch around that await catches it, just like any other error.
Step through a teaching model of await. It uses the lesson's real values. It is not a view inside a real engine.
script
async function serveOrder(order) { const food = await order; return "serving " + food;} When you pause a YouTube video, your whole phone does not pause. You can still use other apps. The player remembers the exact moment where you stopped. When you press play, the video goes on from that moment. It does not start over.
- In real life: The video pauses at one moment
- In JavaScript: One async function pauses at
await - In real life: The player remembers the moment
- In JavaScript: The function keeps its variables and next line
- In real life: Your other phone apps still work
- In JavaScript: Other JavaScript keeps running
- In real life: You press play, and it goes on from there
- In JavaScript: A later job starts the function again at the
await
Native promises and tick markers
Promise jobs wait in a line and run one after another. To see that order, we add tick markers. A tick marker is a promise job that only prints a label, like tick 1.
A tick is not a time in milliseconds. It only shows which job ran before which. So the result is the same on a slow computer and a fast one.
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 makes a native promise that already holds "tea". Line 8 calls waitForTea. Line 4 reaches await, so the function pauses and line 8 is done. Lines 9 to 12 then add three marker jobs, one after the other.
In today's browsers the output is native tea, then tick 1, tick 2, and tick 3.
This does not mean await ran at once. waitForTea had already paused and returned before anything was printed. Its job simply joined the line first, before the marker jobs.
When you need to know the order of jobs in real code, add markers like these. Do not use a clock to guess the order.
How V8 removed extra ticks
A JavaScript engine is the program that runs your code. V8 is the engine inside Chrome and Node.js. Older engines did extra work when you awaited a native promise. They wrapped it in more promises first, and each wrapper cost more jobs.
In version 7.2, V8 made faster async functions. Awaiting a native promise went from three ticks to one. Later, the specification was changed to match, so the short path is now the rule.
This page cannot run an old engine. The example above only shows the order in the browser you are using now. It is a reason to tell native promises apart from other values. It is not a speed test for every engine.
The table compares four kinds of values you can await. Read each row from left to right: what you await, what PromiseResolve does with it, and when the function starts again.
| What you await | What PromiseResolve does | When the function starts again |
|---|---|---|
A plain value, like 42 | It puts the value into a new promise that is already done. | In a later job, after the jobs already in line. |
| A native promise | It uses the promise as it is. | In one job, once the promise is done. This is the short path. |
| A thenable | It makes a new promise that follows the thenable. | After one extra job calls the thenable's then method. |
| A rejected promise | It uses the promise as it is, like any native promise. | In a later job, which throws the error at the await line. |
The main point: a native promise can take the short path, because the engine knows exactly how it behaves. An object with its own then method needs more care. The section Awaiting thenables shows this difference with real output.
A paused function keeps its place
A frame here means the saved state of one call to an async function. Because it can be started again, we call it a resumable frame. It holds:
- the values the function will still need after the
await; - the place to go on from. In our model, this is called the step.
Engines store this in their own ways. The small object below is a model we wrote to teach the idea. It is not code from a real engine.
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 15 makes a frame with step: 0. Line 16 calls resumeServe for the first time. The step is 0, so line 7 sets it to 1, and line 8 returns suspended, which means "paused".
Line 17 calls it again with "burger". Now the step is 1, so the check on line 6 is false and lines 7 and 8 are skipped. Line 10 saves the burger in food, and line 12 returns serving burger. The output is suspended, then serving burger.
The model shows one key fact. A later job does not start the function again from line 1. It goes on from the saved step. That is why a variable made before await can still be used after it.
Each call to an async function gets its own frame. Two separate calls do not share their local variables.
One function, two waits
One call can pause more than once. Each time it starts again, the same frame moves one step forward. The frame keeps the first value while the second value is still on its way. So the last line can use 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 makes the frame. Line 21 runs it to the first wait and prints wait for drink. Line 22 brings "tea". Line 11 saves it in drink, and it prints wait for food.
step: 2 and drink: "tea". Line 23 brings the burger. The tea is still there, so it prints tea and burger.Line 23 brings "burger". Line 15 saves it in food. Line 17 joins both, so it prints tea and burger.
The step-through below runs this same model. A real engine is much faster, and you cannot watch it work. Here you can watch step and the saved values change, one call at a time.
Step through a model we wrote by hand. Engines do not use this exact object, but a paused async function keeps the same kind of information.
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
You can await a value that is not a promise, like await 42. The function still pauses. PromiseResolve puts 42 into a promise that is already done. The function starts again in a later job, so jobs already waiting in line go first.
Because of this, await works the same way whether a function gives back a plain 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 6 calls the function, and it pauses at line 2. Lines 7 to 9 add two marker jobs. The output is value 42, then tick 1, then tick 2.
await first puts 42 into a promise that is already done. The function still pauses. A later job puts 42 into score.The function did start again in a later job. That job simply joined the line before the marker jobs.
This is useful in app code. Say a function gives back a product. If the product was loaded earlier, the function returns it at once. If not, it returns a promise from a network request. Either way, the caller can write await getProduct(), so it needs only one shape of code. It should still handle errors, because a rejected promise becomes a thrown error at the await.
Awaiting thenables
A thenable is any object (or function) with a then method you can call. It looks like a promise, but it does not have to be a real Promise.
Its then method is code that someone else wrote. So JavaScript does not trust it the way it trusts a native promise. Instead, it adds one extra job that calls then later.
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 to 6 make the thenable. Line 13 calls waitForTea, and it pauses at the await on line 9. Lines 14 to 17 add three marker jobs. Then the jobs run in this order:
- The extra job calls the
thenmethod. Line 3 printsthenable then. Line 4 hands over"tea", so the job that starts the function again joins the line now, behindtick 1. - The first marker prints
tick 1. - The function starts again, and line 10 prints
thenable tea. - The last two markers print
tick 2andtick 3.
then first, so tick 1 gets in before the function starts again.Why the extra job? The then method might come from a library. It could throw an error, or hand over a value twice. The promise rules handle those cases safely, and the extra job is part of that.
In your own code, use native promises. If you ever see a strange order of jobs, check whether a thenable is involved.
Compare three kinds of values
Pick one value to await: the number 42, a native promise, or a thenable. Then press Run. The list shows, in order:
- the caller's next line,
caller continues; - two marker jobs,
tick 1andtick 2; - the moment the function starts again,
resume.
Picking another value clears the old list. Reset goes back to 42 with an empty list.
async function waitForScore() { const score = await 42; console.log("value", score);} waitForScore();Promise.resolve() .then(() => console.log("tick 1")) .then(() => console.log("tick 2"));Press Run to see what happens, in order.
Pick one value and press Run. Picking another value clears the old list.
Try 42 first, then the native promise, then the thenable. Each time, ask one question: which jobs run before the function starts again? With the thenable, tick 1 gets in first.
What this changes in app code
You will rarely write promise jobs by hand. But three habits help:
- Do small screen updates, like showing "loading", before the
await. - Return the promise to the code that can show an error if it fails.
- Remember that the rest of the screen still works while one function waits.
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 7 calls showOrder. Line 2 prints loading. Line 3 pauses only showOrder. So line 8 prints screen stays usable, and later line 4 prints show burger.
In a real app, the screen can show a loading message, answer other clicks, or move to another page while the request waits.
Be careful when requests overlap. If a user starts two requests, each call has its own frame. They may finish in a different order from how they started. An old answer could then replace a newer one on the screen. To prevent that:
- cancel the old request, if the tool you use allows it;
- or give each request a number, and only show the answer from the latest one.
The lessons on microtasks and suspended frames go deeper into these ideas.
- Call
serve() - Log
next customer - Put
42into a promise that is already done - Remember
foodand the next line - Run the job that brings a native promise's value
- Call a thenable's
thenin an extra job
Sort each card: does the caller do it, does await do it while pausing, or does it happen later, when the function starts again?
Common misconceptions
- “await freezes JavaScript.” It pauses only the
asyncfunction it is in. Other code keeps running. - “await 42 gives 42 straight away.” A plain value still makes the function pause until a later promise job.
- “A thenable is a Promise.” It only has a
thenmethod you can call. That method is ordinary code that someone wrote. - “A tick count is a time.” Tick markers show the order of jobs, not milliseconds.
- “A saved frame is a browser frame.” Here, a frame is the paused function's saved state: its variables and its next step. It has nothing to do with the pictures a browser draws on the screen.
Some words in this lesson sound like words you may know from elsewhere. Each row gives the word, what it means here, and what it is easy to mix it up with.
| Word | What it means here | Easy to mix up with |
|---|---|---|
await | Pauses the one async function it is in. | Pausing the whole app. The rest of the code keeps running. |
| PromiseResolve | The step that uses a native promise as it is, or puts any other value into a promise. | Calling a thenable's then straight away. That happens in a later job. |
| Frame | A paused function's saved variables and next step. | A picture the browser draws on the screen. |
| Tick marker | One promise job that prints a label, to show the order of jobs. | A time in milliseconds. |
When an output surprises you, trace it in three steps:
- Write down the code that runs now, line by line.
- Write each promise job in the order it joins the line.
- Find the job that starts the async function again.
This works better than calling all later code "callbacks", because it shows exactly where the function paused.
Practice exercises
A food counter starts an order with an async function. Type the first line it prints.
async function serve() {
console.log("order taken");
const food = await "burger";
console.log("serving", food);
}
serve();
console.log("next customer");order taken prints first. Line 2 comes before the await, so it runs straight away, before the caller's next line.
Read the code without running it. What does line 3 print once the function starts again?
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 prints value 42. You can await a plain value. The function pauses, then starts again in a later job with that same value.
A checkout waits for tea, then for a burger. Which saved variable holds the tea while the burger is still on its way?
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"));It is drink (frame.drink in the model). It stays in the frame while the function waits for the food.
A payment library gives you back a thenable, and you await it. Which method does the extra job call to get the result?
The extra job calls the thenable's then method. That method is code someone else wrote, so JavaScript calls it in a separate job.
A user pressed Add to cart, and showOrder is waiting for the order. What should still be usable in the app while this one function waits?
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 paused while its order promise is waiting.
You add tick 1 and tick 2 markers around an await. What do those markers show you?
The markers show the order of promise jobs. They tell you which job ran before which. They say nothing about milliseconds.
Check your understanding
For each question, ask the same three things:
- Which code runs now?
- Which promise jobs join the line, and in what order?
- Which job starts the paused function again?
Question 1 of 7What does this code 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
awaitdo with a plain value such as42?Choose an answer to see the explanation.
Question 3 of 7What is saved while an
asyncfunction is paused?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 show?
Choose an answer to see the explanation.
Question 6 of 7If an awaited promise is rejected, what happens at the
await?Choose an answer to see the explanation.
Question 7 of 7What happens today when you await a native promise?
Choose an answer to see the explanation.
Key takeaways
awaitworks out its value, puts it into a promise if needed, attaches two reactions, and then pauses oneasyncfunction.- The caller gets control back at once. The paused function waits for a later promise job.
- A native promise takes a short path. A thenable needs one extra job, because its
thenis code someone else wrote. - A paused function keeps its place: its variables and its next step. It does not start over from line 1.
await 42still pauses once, so the same code works for plain values and promises.- Tick markers show the order of jobs in one example, not a speed in milliseconds.
Remember the one-liner.await pauses one function, saves its place, and a later promise job starts it again from there.
Coming next: The microtask queue. It shows the line where promise jobs wait before they run.