completefrontend
Open lab My progress
THE JAVASCRIPT FIELD GUIDE

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.

By the end, you can
  • 01
    Know what an async function gives backExplain why every async function gives back a promise, and what that promise holds after a return or a throw.
  • 02
    Predict 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.
  • 03
    Catch errors from awaited codeUse try, catch, finally, and return await in 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.

The main idea

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, await gives you its value.
  • If the promise failed, await throws 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.

Real-life analogyAwait is a bookmark

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 await line
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:

  1. An async function always gives back a promise.
  2. await pauses one function and carries on later.
  3. Errors can be caught with try and catch.
  4. What await does, written with .then.
  5. await outside 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

INTERACTIVE

You 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.

An async call gives back a promisePop out in the code editor (opens in a new tab)JavaScript
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.

A function card greet() has an arrow to a box called result. The box holds a Promise card, labelled right away. An arrow marked .then goes from the box to a card that says Asha, labelled later.greet()callPromiseresultright away.then"Asha"later
Calling greet() puts a promise in result right away. The name "Asha" arrives later, inside .then.
Real-life analogyAsync functions are a gift-wrapping counter

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 with 42
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 then method: JavaScript calls then and 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 then method. 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:

  • nothing returns no value, so the promise is fulfilled with undefined.
  • promise returns a promise. You don't get that same promise back. You get a new promise that adopts its result.
What does an async function return?
One function, five ways to finishPop out in the code editor (opens in a new tab)JavaScript
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);
The promise it gave backvalue
stateready
value or errorClick Run
Try it yourself

Choose what the async function does, then click Run. It runs in your browser.

Each button picks one line of the function to run. Watch the function's promise get fulfilled, get rejected, or take the result of a returned promise.

The table sums up the playground. Each row is one way to leave an async function.

What happens to the promise when an async function finishes
What the function doesWhat the promise becomesWhat value or error it holds
return 42It 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 returnIt 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:

  • await the call.
  • Return the promise to code that will await it.
  • Add .catch(...) to the call on purpose.

Await pauses one function, not the whole page

STEP THROUGH

The most important pattern in this lesson is the print order A, C, B:

  1. The async function prints A before its await.
  2. The function pauses. The code that called it, the caller, carries on and prints C.
  3. Later, the async function carries on and prints B.
Real-life analogyAn order number at a burger shop

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 await with 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
Pause and carry on: A, C, B
Step 0 of 6Ready
Your turn: follow the blue line

Guess the order first. Then step through it: the async function starts now, pauses at await, and carries on later.

Running in
  1. script
Next: line 7
async function demo() {  console.log("A: before await");  const value = await Promise.resolve("B");  console.log(value + ": after await");} console.log("C: caller continues");
CallStoreChangeResultBlue line: runs next. Ran: already executed.
Recent returnsNothing yet. Start with the blue line.
Choose what comes after await on line 3.

All three pause the async function. The timer makes it wait longer, but the order is still A, C, B.

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.
Two rows. The top row is demo(), the bottom row is the caller. demo() prints A before await. An arrow goes down to the caller, which prints C. An arrow goes back up to demo(), which prints B after await. Between A and B, demo() is paused.demo()callerbefore awaitApausedCcaller goes onBafter await
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.

Variables keep their values during the pause

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.

A variable keeps its value across awaitPop out in the code editor (opens in a new tab)JavaScript
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 THROUGH

Code 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.

try, catch, and finally with two awaited steps
Step 0 of 6Ready
Your turn: follow the blue line

Choose which step fails. Then step through to see what try, catch, and finally do.

Running in
  1. script
Next: line 1
  try {    const user = await step("user", failStep);    const posts = await step("posts", failStep);    return user + " + " + posts;  } catch (error) {    return "caught " + error.message;  } finally {    cleanup();  }}
CallStoreChangeResultBlue line: runs next. Ran: already executed.
Recent returnsNothing yet. Start with the blue line.
Which awaited step fails?

Each choice records a new run. The catch only gets a failure that happens on an await line inside the try.

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.

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.

Return without await skips the catchPop out in the code editor (opens in a new tab)JavaScript
async function notCaught() {  try {    return fetchData();  } catch (error) {    return "caught";  }} async function caught() {  try {    return await fetchData();  } catch (error) {    return "caught";  }}
Return and return await, side by sidePop out in the code editor (opens in a new tab)JavaScript
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.

Left: return fetchData(). The rejected promise goes around the catch box and escapes, so catch is skipped. Right: return await fetchData(). The rejected promise drops straight into the catch box, so it is caught.return fetchData()rejectedcatchcatch skippedescapesreturn await fetchData()rejectedcatchcaught
Without 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.

Forgetting await in an ifPop out in the code editor (opens in a new tab)JavaScript
if (checkPermission()) {  showAdminButton(); // runs: a Promise object is truthy} if (await checkPermission()) {  showAdminButton(); // waits for the boolean}
The promise is not the answerPop out in the code editor (opens in a new tab)JavaScript
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.

Left: if (checkPermission()). The if sees a Promise card. An object counts as true, so the branch runs. Right: if (await checkPermission()). The if sees a false card, the real answer, so the branch is skipped.if (checkPermission())Promisean object counts as truebranch runsif (await checkPermission())falsethe real answerbranch skipped
Without 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

COMPARE

The 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:

  1. It turns value into a promise with Promise.resolve(value).
  2. It saves where the function is and what its variables hold.
  3. 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.

The same function with await and with .thenPop out in the code editor (opens in a new tab)JavaScript
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:

  • await doesn't stop the thread. It only hands the rest of the function to a promise.
  • An error after await rejects 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.

await and .then compared
QuestionWith awaitWith 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

SANDBOX

Since 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.

Top-level await: module script and classic script
The two scripts the iframes runPop out in the code editor (opens in a new tab)HTML
<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>
What each script reportedreal iframes
module scriptwaiting

The module frame sends its result after its top-level await is done.

classic scriptwaiting

The classic frame reports whether the browser could read its top-level await.

Try it yourself

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.

The iframes are sandboxed: they can run scripts, but nothing else. The browser console may allow top-level await to make trying things easier; a classic script file does not.

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.

Can await go here?
  • 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); }
Try it yourself
0 of 7 correct

Sort each place by whether await is allowed there, and whether it waits the way people expect.

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

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.

Load a dashboard, with loading and error messagesPop out in the code editor (opens in a new tab)JavaScript
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.

Load a profile and greet the userPop out in the code editor (opens in a new tab)JavaScript
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.

A loop that waits, and Promise.allPop out in the code editor (opens in a new tab)JavaScript
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 await inside the try. 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, but forEach doesn'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 EXERCISES
Exercise 1 · Warm-upPredict the pause

Read the code. Type the three words in the order they print.

Starter codePop out in the code editor (opens in a new tab)JavaScript
async function run() {
  console.log("one");
  await 0;
  console.log("three");
}
run();
console.log("two");

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

    Exercise 2 · Warm-upWhat comes out of the promise?

    What value does the function inside .then print?

    Starter codePop out in the code editor (opens in a new tab)JavaScript
    async function answer() {
      return 12;
    }
    answer().then((value) => console.log(value));

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

      Exercise 3 · PracticeFind the error that gets away

      Type the word that prints. Then explain why the catch inside demo did not run.

      Starter codePop out in the code editor (opens in a new tab)JavaScript
      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));

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

        Exercise 4 · PracticeFix the catch

        After adding await, what word prints?

        Starter codePop out in the code editor (opens in a new tab)JavaScript
        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));

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

          Exercise 5 · ChallengeFix the permission bug

          Type the missing keyword that makes the if wait for the real answer.

          Starter codePop out in the code editor (opens in a new tab)JavaScript
          async function renderAdminTools() {
            if (checkPermission()) {
              showAdminButton();
            }
          }

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

            Check your understanding

            7 QUESTIONS
            Lesson quiz · 7 questionsScore: first tries count
            1. Question 1 of 7What does calling an async function always give back straight away?

              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
              async function demo() {
                console.log("A");
                await "B";
                console.log("C");
              }
              demo();
              console.log("D");

              Choose an answer to see the explanation.

            3. Question 3 of 7Why can try { return await fetchData(); } catch { ... } catch a rejection when return fetchData() cannot?

              Choose an answer to see the explanation.

            4. Question 4 of 7This async function throws. What does this code print?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              async function fail() {
                throw new Error("boom");
              }
              fail().catch((error) => console.log(error.message));

              Choose an answer to see the explanation.

            5. Question 5 of 7Where does top-level await work in standard JavaScript?

              Choose an answer to see the explanation.

            6. Question 6 of 7What is a good rough picture of await value?

              Choose an answer to see the explanation.

            7. Question 7 of 7What is wrong with if (checkPermission()) when checkPermission gives back a promise?

              Choose an answer to see the explanation.

            Key takeaways

            • Calling an async function gives back a new promise straight away.
            • A return fulfills that promise. A throw rejects it. A returned promise is adopted: the outer promise takes its result.
            • await pauses the one async function it is in. The rest of JavaScript keeps running.
            • Even await on a plain value carries on later, not straight away.
            • Put try/catch around await lines. Inside try, write return await so that catch can see the error.
            • Top-level await works 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.

            CompleteFrontend Clear concepts. Working examples.