JavaScript Async Await

In the previous lesson, we learned how promises represent a value that arrives later and how .then() reads that value. Now let’s learn async/await, a cleaner way to work with the same promises that reads like ordinary top-to-bottom code.

⚡ What are Async and Await?

async/await is built on top of promises. It does not replace them; it gives you a nicer way to write them. The async keyword marks a function as asynchronous, and the await keyword waits for a promise inside that function.

Keyword Meaning Where it goes
async Marks a function as asynchronous and makes it return a promise Before the function keyword
await Pauses until a promise resolves, then gives you its value Inside an async function

An async function always returns a promise, even if you return a plain value:

async-await.js
async function getName() {
return "Alex";
}
getName().then((name) => console.log(name)); // Alex

Let’s walk through what each line does:

  • async function getName() marks getName as asynchronous, so its return value is automatically wrapped in a promise.
  • return "Alex" returns a plain string, but because the function is async, the string comes back as a resolved promise that holds "Alex".
  • getName().then((name) => ...) calls the function and uses .then() on the returned promise to read the value once it resolves, logging Alex.

The string "Alex" is automatically wrapped in a resolved promise, so you can call .then() on the result.

⏳ The await Keyword

Inside an async function, await pauses the function until the promise settles and then hands you the resolved value. The rest of the function waits for that line to finish before it runs.

async-await.js
async function showUser() {
const user = await fetchUser(); // pause here until the promise resolves
console.log(user.name); // runs only after the value arrives
}

Here is how the function runs line by line:

  • await fetchUser() calls fetchUser, which returns a promise, and pauses the function until that promise resolves.
  • const user = ... stores the resolved value (not the promise) once the pause ends.
  • console.log(user.name) runs only after the value arrives, so user is ready to use.

The variable user holds the resolved value, not the promise itself. This is what makes await feel like synchronous code: each line waits for the one before it.

await needs async

You can only use await inside a function declared with async. Using it anywhere else is a syntax error.

🔁 The Same Code: .then() vs async/await

The two snippets below do exactly the same thing: fetch a user, then print the name. The first uses .then() chaining, and the second uses async/await.

With .then(), each step lives inside a callback:

async-await.js
function showUser() {
fetchUser().then((user) => {
console.log(user.name);
});
}

Let’s trace this version:

  • fetchUser() starts the work and returns a promise.
  • .then((user) => {...}) registers a callback that runs later, once the promise resolves.
  • console.log(user.name) runs inside that callback, with user being the resolved value.

With async/await, the same logic reads top to bottom:

async-await.js
async function showUser() {
const user = await fetchUser();
console.log(user.name);
}

Here is how the two versions map to each other:

  • await fetchUser() replaces the .then() call. Instead of passing a callback, await pauses and returns the resolved value directly.
  • const user = ... plays the role of the user parameter in the .then() callback. Both hold the same resolved value.
  • console.log(user.name) is the same line as inside the callback, but now it sits on the next line instead of being nested.

Both call fetchUser(), wait for the result, and print the name. The async/await version avoids the nested callback and reads in the same order it runs.

🛡️ Handling Errors with try/catch

A promise can reject when something goes wrong. With async/await, you catch a rejected promise using a normal try/catch block, the same way you handle other errors in JavaScript.

async-await.js
async function showUser() {
try {
const user = await fetchUser(); // ✅ if this rejects, control jumps to catch
console.log(user.name);
} catch (error) {
console.log("Something went wrong:", error.message);
}
}

Let’s follow both paths through this code:

  • try { ... } wraps the lines that might fail so a rejection can be caught.
  • await fetchUser() either resolves and lets the next line run, or rejects and throws an error.
  • console.log(user.name) runs only when the promise resolves successfully.
  • catch (error) { ... } runs instead when the promise rejects, and error.message describes what went wrong.

If the awaited promise rejects, the await line throws and the catch block runs. We will go deeper into error handling in the next-next lesson on error handling.

Always wrap risky awaits

Any await that can reject belongs inside try/catch. Without it, a rejected promise becomes an unhandled error.

🧪 A Practical Example

Here we wait for a delayed promise and use its result. The delay function returns a promise that resolves after a set time, and loadMessage awaits it.

async-await.js
function delay(ms, value) {
return new Promise((resolve) => {
setTimeout(() => resolve(value), ms);
});
}
async function loadMessage() {
console.log("Loading...");
const message = await delay(1000, "Data ready!"); // wait one second
console.log(message); // Data ready!
}
loadMessage();

Let’s step through the example from top to bottom:

  • delay(ms, value) returns a new promise and uses setTimeout to call resolve(value) after ms milliseconds, so the promise resolves with value once the time passes.
  • console.log("Loading...") runs first and prints Loading... right away.
  • await delay(1000, "Data ready!") pauses loadMessage for one second, then stores the resolved value "Data ready!" in message.
  • console.log(message) prints Data ready! after the pause ends.
  • loadMessage() calls the function to start the whole sequence.

When loadMessage runs, it prints Loading..., pauses one second at the await line, and then prints Data ready!. The value passed to resolve becomes the value of the await expression.

⚠️ Common Mistakes to Avoid

Mistake Problem Solution
Using await outside an async function JavaScript throws a syntax error Put the await inside a function marked async
Forgetting await You get a Promise object instead of the value Add await before the call that returns a promise
Not wrapping await in try/catch A rejected promise becomes an unhandled error Surround risky awaits with try/catch

Forgetting await is the most common slip. Compare these two lines:

async-await.js
const user = fetchUser(); // ❌ user is a Promise, not the value
const user = await fetchUser(); // ✅ user is the resolved value

The difference comes down to one keyword:

  • The first line skips await, so user holds the pending Promise object and user.name would be undefined.
  • The second line adds await, so the function pauses and user holds the resolved value, ready to use.

🔧 Try It Yourself!

  1. Write a delay function that returns a promise resolving after one second.
  2. Create an async function that awaits delay and logs the result.
  3. Rewrite a .then() chain you have seen as an async/await function.
  4. Wrap an await in try/catch and make the promise reject to see the catch run.

🧩 What You’ve Learned

  • async/await is a cleaner way to work with promises that reads like synchronous code
  • ✅ An async function always returns a promise
  • await pauses inside an async function until a promise resolves and gives its value
  • ✅ You can only use await inside an async function
  • try/catch handles a rejected promise in async/await code

🚀 What’s Next?

Now that you can write asynchronous code that reads cleanly, let’s use it to request real data from the web. Let’s continue to Fetch API.

Share & Connect