async & await
Write promise code that reads from top to bottom: async functions, await pauses, error handling, top-level await, and the promise machinery underneath.
- 01Know what an async function gives backExplain why every async function gives back a promise, and what that promise holds after a
returnor athrow. - 02Predict the order with awaitStep through a pause: the caller carries on first, then the async function carries on, and its variables still hold their values.
- 03Catch errors from awaited codeUse
try,catch,finally, andreturn awaitin the places where they change what happens.
Promise code you can read from top to bottom
A promise is an object that stands for a value that arrives later, such as data from a server. You already know how to wait for a promise with .then(...): you pass it a function, and that function runs when the value is ready.
async and await are two keywords that do the same job with less nesting. You write the steps one under the other, and they read from top to bottom, like normal code.
An async function is a function with the word async in front. Calling it always gives you back a promise.
Inside it, await pauses that one function until a promise is ready. Then the function carries on from the same line:
- If the promise worked,
awaitgives you its value. - If the promise failed,
awaitthrows its error on that line.
The pause is the part to understand well. It does not freeze the browser tab. Clicks still work, and timers still run. Only one async function waits. The rest of your JavaScript keeps going. When the promise is ready, the paused function goes back to the exact line where it stopped.
This lesson builds on Promises, the Event loop, and Microtasks. If those feel new, have a quick look at them first.
You are following a recipe. At step four, the dough must rise for an hour. You put a bookmark on step four and leave the kitchen. When you come back, the bowl and your notes are still there, and you carry on from step four. await is that bookmark for one async function.
- In real life: Put a bookmark on step four of a recipe
- In JavaScript: The function reaches an
awaitline - In real life: Leave the kitchen and do something else
- In JavaScript: JavaScript runs other code that is ready
- In real life: Come back to step four; the bowl is still there
- In JavaScript: The function carries on, and its variables still hold their values
The lesson goes in this order:
- An async function always gives back a promise.
awaitpauses one function and carries on later.- Errors can be caught with
tryandcatch. - What
awaitdoes, written with.then. awaitoutside any function, in modules.
There is no second thread in any of this. A thread is one line of work the computer runs. All of this lesson's code runs on JavaScript's one thread.
An async function always gives back a promise
INTERACTIVEYou can put async in front of any kind of function: a normal function, an arrow function, or a method. Calling it always gives back a new promise.
The function's code starts running straight away. It runs until one of three things happens: it returns, it throws an error, or it reaches its first await.
async function greet() {
return "Asha";
}
const result = greet();
console.log(result instanceof Promise);
result.then((name) => console.log(name));Line 1 makes greet an async function. Line 2 returns the name "Asha".
Line 5 calls greet(). But result holds a promise, not the name. Line 6 checks this with instanceof Promise, which asks "is this a promise?". It prints true.
Line 7 waits with .then. When the promise is ready, the function inside .then gets the name, so it prints Asha.
greet() puts a promise in result right away. The name "Asha" arrives later, inside .then.Everything that leaves an async function gets wrapped in a promise box. A value makes a box that worked. A thrown error makes a box marked broken. If you hand over a box that is already wrapped, the counter doesn't put a box inside a box. It just passes on what was inside.
- In real life: Hand over a toy
- In JavaScript:
return 42: the promise is fulfilled with42 - In real life: Hand over a gift another counter already wrapped
- In JavaScript:
return somePromise: the promise waits for that promise and takes its result - In real life: Hand over a claim ticket for a gift
- In JavaScript: Return an object with a
thenmethod: JavaScript callsthenand takes its result - In real life: Drop the gift and mark the box broken
- In JavaScript:
throw error: the promise is rejected with that error
A few words you will see in this lesson:
- Fulfilled: the promise worked and has a value.
- Rejected: the promise failed and has an error. The error is called the reason.
- Settled: the promise is either fulfilled or rejected. It is no longer waiting.
- Thenable: any object with a
thenmethod. JavaScript treats it like a promise. - Adopt: when an async function returns a promise, its own promise waits for that one and takes the same result.
Try each choice below. The code stays the same; you pick which line runs, and it runs in your browser. Look for two things:
nothingreturns no value, so the promise is fulfilled withundefined.promisereturns a promise. You don't get that same promise back. You get a new promise that adopts its result.
async function gift(kind) { if (kind === "value") return 42; if (kind === "promise") return Promise.resolve("ready"); if (kind === "thenable") return { then(resolve) { resolve("thenable value"); } }; if (kind === "throw") throw new Error("broken ribbon");} gift(kind);Choose what the async function does, then click Run. It runs in your browser.
The table sums up the playground. Each row is one way to leave an async function.
| What the function does | What the promise becomes | What value or error it holds |
|---|---|---|
return 42 | It is fulfilled. | The value 42. |
return Promise.resolve("ready") | It is fulfilled once the returned promise is fulfilled. | The value "ready". |
return { then(resolve) { ... } } | It is fulfilled once the object's then method passes a value. | Whatever value then passes on. |
throw new Error("broken") | It is rejected. | The thrown error. |
Ends with no return | It is fulfilled. | The value undefined. |
An async call starts running even if you never await it. If it throws later and no code handles the promise, the browser or Node shows an "unhandled rejection" error. To avoid that, do one of these:
awaitthe call.- Return the promise to code that will
awaitit. - Add
.catch(...)to the call on purpose.
Await pauses one function, not the whole page
STEP THROUGHThe most important pattern in this lesson is the print order A, C, B:
- The async function prints A before its
await. - The function pauses. The code that called it, the caller, carries on and prints C.
- Later, the async function carries on and prints B.
At a burger shop, you order, take a number, and sit down. The counter keeps serving other people. When your number shows on the screen, you collect your food. await works the same way: only your function waits, and JavaScript keeps serving other code.
- In real life: You order and get a number
- In JavaScript: The function reaches
awaitwith a promise that isn't ready - In real life: The counter serves other people
- In JavaScript: JavaScript runs other code that is ready
- In real life: Your number shows on the screen
- In JavaScript: The promise is ready, so the function is lined up to carry on
- In real life: You collect your food
- In JavaScript: The function carries on with the value
Guess the order first. Then step through it: the async function starts now, pauses at await, and carries on later.
script
async function demo() { console.log("A: before await"); const value = await Promise.resolve("B"); console.log(value + ": after await");} console.log("C: caller continues");demo() prints A, then pauses at await. The caller prints C. Then demo() comes back and prints B.Try all three choices. Each one puts something different after await:
- a promise that is already fulfilled,
- a promise that a timer fulfills after 300 milliseconds,
- a plain value,
"B", that is not a promise at all.
All three pause the rest of the function. The timer changes how long the function waits. It doesn't change the order.
Even await 0 pauses. JavaScript first turns the value into a promise with Promise.resolve(0), then carries on later. Don't try to count exactly how many steps the pause takes; browsers have changed that over the years. The rule to remember: code after await runs later, after the caller has carried on.
When an async function pauses, JavaScript saves its variables. That is why you can write const user = await getUser(); and then use user on the next line, as if the function had simply waited there.
async function readOrder() {
const order = "tea";
await Promise.resolve();
console.log(order);
}
readOrder();
console.log("counter stays open");Line 2 stores "tea" in order before the pause. Line 3 pauses readOrder.
While it is paused, line 8 runs and prints counter stays open first. Then line 4 runs. order still holds its value, so it prints tea.
Catching errors with try and catch
STEP THROUGHCode inside a try block can fail. When it fails, JavaScript jumps to the catch block. A finally block runs at the end either way.
An awaited promise that is rejected inside a try acts like a throw on that line. So catch and finally work for waiting code, the same as for normal code.
Choose which step fails. Then step through to see what try, catch, and finally do.
script
try { const user = await step("user", failStep); const posts = await step("posts", failStep); return user + " + " + posts; } catch (error) { return "caught " + error.message; } finally { cleanup(); }}There are two common mistakes. The first one: return fetchData() inside a try returns the promise without waiting for it.
If that promise is rejected later, the function has already left the try. So the catch never runs. Write return await fetchData() when you want that catch to handle the failure, or when you need finally to wait until the promise is settled.
async function notCaught() { try { return fetchData(); } catch (error) { return "caught"; }} async function caught() { try { return await fetchData(); } catch (error) { return "caught"; }}function fetchData() {
return Promise.reject(new Error("nope"));
}
async function returned() {
try {
return fetchData();
} catch {
return "inner";
}
}
async function awaited() {
try {
return await fetchData();
} catch {
return "inner";
}
}
returned().catch((error) => console.log("return", error.message));
awaited().then((value) => console.log("await", value));returned() returns the rejected promise on line 7. Its own catch doesn't see the error. The .catch on line 21 gets it instead and prints return nope.
awaited() waits on line 15, inside try. The rejection becomes a throw there. Its own catch returns "inner", so line 22 prints await inner.
await, the rejected promise leaves the function and the catch never sees it. With await, the rejection happens inside try, so catch gets it.Outside a try block, you usually don't need return await. Returning the promise is enough. Inside a try block, it decides where the error goes: to this function's catch, or out to the caller.
The second mistake: forgetting await can make a check wrong without any error.
An if treats every object as true; the word for that is truthy. A promise is an object. So if (checkPermission()) always runs its block, before the real true or false arrives.
if (checkPermission()) { showAdminButton(); // runs: a Promise object is truthy} if (await checkPermission()) { showAdminButton(); // waits for the boolean}function checkPermission() {
return Promise.resolve(false);
}
async function showResult() {
const permission = checkPermission();
console.log(permission instanceof Promise);
console.log(await permission);
}
showResult();Line 6 stores the promise itself in permission, so line 7 prints true: it is a promise. Line 8 adds await, gets the real answer, and prints false.
await, the if checks the promise object, which always counts as true. With await, it checks the real answer, false.What await does, written with .then
COMPAREThe JavaScript engine is the program that runs your code, such as the one in your browser. Each engine has its own tricks to make await fast, so don't take this too literally. But as a rough picture, await value does three things:
- It turns
valueinto a promise withPromise.resolve(value). - It saves where the function is and what its variables hold.
- When the promise is settled, it runs the rest of the function.
"The rest of the function" has a name: the continuation. It is the code still waiting to run after the await.
async function label() { const user = await getUser(); return "Hi, " + user.name;} function labelRoughly() { return Promise.resolve(getUser()).then((user) => { return "Hi, " + user.name; });}In label, line 2 waits for the user, and line 3 is the continuation. In labelRoughly, line 7 does the same with .then: the continuation, line 8, is inside the function passed to .then.
The two versions are not exactly the same in every detail. But the .then version explains two facts:
awaitdoesn't stop the thread. It only hands the rest of the function to a promise.- An error after
awaitrejects the function's promise, just like an error inside.then.
Async functions can stop in the middle, keep their variables, and carry on later. Generator functions, which you may meet later, can do this too.
The table compares the two ways. Read each row as a question, then the answer for each way.
| Question | With await | With a .then chain |
|---|---|---|
| Where does the next step go? | On the next line of the async function. | Inside the function you pass to .then. |
| How do you catch a failure? | Put try/catch around the await lines. | Add .catch, or pass a second function to .then. |
| Does it stop the thread? | No. | No. |
| What does the outer function give back? | A promise. | Usually the promise the chain makes. |
Await outside any function
SANDBOXSince 2022, JavaScript lets you use await outside any function, at the top of the file. This is called top-level await. It only works in an ES module: a file loaded with <script type="module"> or with import.
A normal <script> without type="module" is called a classic script. In a classic script, top-level await is a syntax error.
The browser console and the Node prompt often let you use top-level await anyway, to make trying things easier. That doesn't mean it works in a classic script file.
<script type="module"> const data = await Promise.resolve("module data"); parent.postMessage({ kind: "module", value: data }, "*");</script> <script> await Promise.resolve("classic data"); // SyntaxError</script>The module frame sends its result after its top-level await is done.
The classic frame reports whether the browser could read its top-level await.
Two small hidden pages, called iframes, each run one script. Each one sends its result back to this page with postMessage. The module script waits and works. The classic script reports a syntax error in most browsers.
Top-level await pauses the module. Any file that imports that module waits too. This helps when a module must load something first, such as its settings.
Use it with care. If the awaited work is slow, every file that imports the module has to wait for it.
- Inside the code of an
async function - At the top of an ES module, outside any function
- At the top of a classic
<script>, outside any function items.forEach((x) => { await save(x); })items.forEach(async (x) => { await save(x); })const load = async () => await getData();async save() { await api.save(this); }
Sort each place by whether await is allowed there, and whether it waits the way people expect.
Where you’ll use this
You use async and await whenever code must wait for something that isn't ready yet:
- data from a server,
- a file from the disk,
- an animation finishing,
- a permission question the user must answer,
- a timer.
The usual steps are: show "Loading...", await the work, show the result, and handle failure.
async function showDashboard() { setStatus("Loading..."); try { const user = await getUser(); const projects = await getProjects(user.id); renderDashboard(user, projects); } catch (error) { showError("Could not load the dashboard."); } finally { hideSpinner(); }}Line 2 shows a loading message. Line 4 waits for the user. Line 6 shows the dashboard. If anything inside try fails, line 8 shows an error. Line 10 hides the spinner in both cases.
The two await lines run one after the other on purpose: getProjects needs user.id, so it must wait for the user first.
function getUser() {
return Promise.resolve({ name: "Asha" });
}
async function showProfile() {
const user = await getUser();
console.log("Hello, " + user.name);
}
showProfile();Here getUser is a pretend version, so the example works without the internet. But it has the same steps as real code: line 6 waits for the data, then line 7 shows the greeting. It prints Hello, Asha.
Sometimes two requests don't need each other. Then you can start them together and wait for both at once.
const items = ["tea", "toast"];
async function save(item) {
return item + " saved";
}
async function run() {
for (const item of items) {
console.log(await save(item));
}
const saved = await Promise.all(items.map(save));
console.log(saved.join(", "));
}
run();The for...of loop waits for each item before it starts the next one. It prints tea saved, then toast saved.
Promise.all takes a list of promises and gives back one promise. That promise is fulfilled when all of them are fulfilled. Here, both save calls start first, then line 11 waits for both. Line 12 prints tea saved, toast saved.
The next lesson, Sequential vs parallel, covers timing and the forEach(async ...) mistake. Later, the course shows for await...of, a loop for values that arrive one at a time.
Common misconceptions
- "async makes the function run on another thread." No. The function runs on JavaScript's one thread. At each
await, it steps aside so other code can run. - "await stops the whole page." No. It pauses one async function. Other code that is ready still runs.
- "Awaiting a value that isn't a promise happens straight away." No. The function still pauses and carries on later.
- "try/catch catches every promise returned inside it." No. It only catches a rejection you
awaitinside thetry. Returning a promise is not the same as waiting for it. - "await inside forEach makes forEach wait." No. You may pass an async function to
forEach, butforEachdoesn't wait for its promises. The next lesson shows loops that do wait. - "Top-level await works in any script." No. It works in modules, not in classic scripts.
Practice exercises
5 EXERCISESRead the code. Type the three words in the order they print.
async function run() {
console.log("one");
await 0;
console.log("three");
}
run();
console.log("two");It prints one, then two, then three. one prints before the await. Then the caller carries on and prints two. Last, the function carries on and prints three.
What value does the function inside .then print?
async function answer() {
return 12;
}
answer().then((value) => console.log(value));.then gets 12. In an async function, return 12 fulfills the promise with 12.
Type the word that prints. Then explain why the catch inside demo did not run.
function fetchData() {
return Promise.reject(new Error("nope"));
}
async function demo() {
try {
return fetchData();
} catch (error) {
return "caught";
}
}
demo().catch((error) => console.log(error.message));It prints nope. Line 6 returns fetchData() without await, so the error doesn't happen inside the try. The catch inside demo never runs, and the .catch on the last line gets the error.
After adding await, what word prints?
function fetchData() {
return Promise.reject(new Error("nope"));
}
async function demo() {
try {
return await fetchData();
} catch (error) {
return "caught";
}
}
demo().then((value) => console.log(value));It prints caught. The rejection now happens inside the try block, so catch handles it and returns the word "caught".
Type the missing keyword that makes the if wait for the real answer.
async function renderAdminTools() {
if (checkPermission()) {
showAdminButton();
}
}async function renderAdminTools() {
if (await checkPermission()) {
showAdminButton();
}
}Put await inside the condition. Then the if gets the real true or false instead of the promise object, which always counts as true.
Check your understanding
7 QUESTIONSQuestion 1 of 7What does calling an async function always give back straight away?
Choose an answer to see the explanation.
Question 2 of 7What does this print?
Read the code, then predictasync function demo() { console.log("A"); await "B"; console.log("C"); } demo(); console.log("D");Choose an answer to see the explanation.
Question 3 of 7Why can
try { return await fetchData(); } catch { ... }catch a rejection whenreturn fetchData()cannot?Choose an answer to see the explanation.
Question 4 of 7This async function throws. What does this code print?
Read the code, then predictasync function fail() { throw new Error("boom"); } fail().catch((error) => console.log(error.message));Choose an answer to see the explanation.
Question 5 of 7Where does top-level await work in standard JavaScript?
Choose an answer to see the explanation.
Question 6 of 7What is a good rough picture of
await value?Choose an answer to see the explanation.
Question 7 of 7What is wrong with
if (checkPermission())whencheckPermissiongives back a promise?Choose an answer to see the explanation.
Key takeaways
- Calling an async function gives back a new promise straight away.
- A
returnfulfills that promise. Athrowrejects it. A returned promise is adopted: the outer promise takes its result. awaitpauses the one async function it is in. The rest of JavaScript keeps running.- Even
awaiton a plain value carries on later, not straight away. - Put
try/catcharoundawaitlines. Insidetry, writereturn awaitso thatcatchcan see the error. - Top-level
awaitworks in modules. In a classic script, it is a syntax error.
In one line: async and await let one function pause until a promise is ready, while the rest of JavaScript keeps running.
Up next: Sequential vs parallel.