What you'll learn
Quick Answer
async/await is modern JavaScript syntax for working with asynchronous code — tasks like network requests that finish later. Marking a function async lets you use await inside it, which pauses until a Promise settles and hands you the result, so the code reads top to bottom instead of nesting callbacks. Under the hood it is still built on Promises; await just unwraps the value for you. Wrap your await calls in try/catch to handle errors cleanly.
Why JavaScript Needs Asynchronous Code
JavaScript runs on a single thread, which means it can only do one thing at a time. That is fine for quick work like adding two numbers, but some jobs — fetching data from a server, reading a file, or waiting for a timer — take a while to finish. If JavaScript simply stopped and waited for each of these, your whole page would freeze until they were done.
The solution is asynchronous code: JavaScript starts a slow task, keeps running everything else, and deals with the result once it is ready. This guide to javascript async await builds the idea up in three steps — first callbacks, then Promises with then and catch, and finally async/await, which lets asynchronous code read cleanly from top to bottom. Each step fixes a real problem with the one before it, so the progression is worth following in order.
Starting Point: Callbacks (and Callback Hell)
The oldest way to handle "do this once the slow task is done" is a callback — a function you pass in that JavaScript calls later. You have probably already seen one with setTimeout:
setTimeout(() => {
console.log("This runs after 1 second");
}, 1000);
console.log("This runs first");Look at the order of the output: "This runs first" prints immediately, and the callback fires a second later. That is asynchronous code in action — JavaScript did not sit and wait.
Callbacks work, but they get ugly fast. When one async task depends on the result of another, you end up nesting callbacks inside callbacks — a mess developers nicknamed callback hell or the "pyramid of doom":
getUser(1, (user) => {
getPosts(user.id, (posts) => {
getComments(posts[0].id, (comments) => {
console.log(comments);
// ...and it keeps drifting right
});
});
});The code marches further to the right with every step, and handling errors at each level becomes painful. Promises were created to fix exactly this.
Promises: a Cleaner Way with then and catch
A Promise is an object that represents a value that is not ready yet. Think of it like a receipt from a restaurant: you do not have your food, but you hold a guarantee that you will get either the meal or an apology. A Promise is always in one of three states — pending (still waiting), fulfilled (it worked, here is the value), or rejected (something went wrong).
You react to a Promise with two methods: then runs when it is fulfilled, and catch runs when it is rejected. The browser's built-in fetch function returns a Promise, so it makes a perfect example:
fetch("https://api.github.com/users/torvalds")
.then((response) => response.json())
.then((user) => console.log(user.name))
.catch((error) => console.error(error));The big win over callbacks is chaining. Each then returns a new Promise, so instead of nesting to the right you stack steps neatly downward, and a single catch at the end handles an error from any step above it. This is a real improvement — but there is a version that reads even more naturally.
How JavaScript async/await Works
Introduced in ES2017, async and await are two keywords that let you write Promise-based code as if it were ordinary, step-by-step code. Nothing new happens under the hood — async/await is built on top of Promises — but the syntax is far easier to read.
There are just two rules:
- Put
asyncbefore a function to mark it as asynchronous. Anasyncfunction always returns a Promise. - Inside an
asyncfunction, putawaitbefore a Promise. It pauses that function until the Promise settles, then hands you the resolved value directly.
Watch the same fetch example become almost boringly linear:
async function showName() {
const response = await fetch("https://api.github.com/users/torvalds");
const user = await response.json();
console.log(user.name);
}
showName();Read it top to bottom: get the response, turn it into JSON, log the name. No then, no nesting, no callbacks — just three lines that happen to wait when they need to. Importantly, await only pauses this function; the rest of your page keeps running, so nothing freezes.
A Real Example: Fetching Data with fetch()
Let us build one complete, working function you can paste into your browser console. We will look up a GitHub user and print their name — with one gotcha handled that trips up almost every beginner.
async function getUser(username) {
const response = await fetch(`https://api.github.com/users/${username}`);
// fetch does NOT reject on 404 or 500 — you must check yourself
if (!response.ok) {
throw new Error(`Request failed with status ${response.status}`);
}
const user = await response.json();
return user;
}
getUser("torvalds").then((user) => console.log(user.name));Two things are worth calling out. First, because getUser is async, it returns a Promise, so we use then (or another await) to read its result. Second, and this surprises people: fetch only rejects on a network failure, not on a bad HTTP status. A 404 or 500 still counts as a "successful" response as far as fetch is concerned, so you check response.ok yourself and throw if something is wrong. That thrown error is exactly what we handle next.
Handling Errors with try/catch
With callbacks you passed an error argument around by hand. With async/await you use the same try/catch you already use for normal code. Anything that throws inside the try block — including a rejected await — jumps straight to catch:
async function showUser(username) {
try {
const response = await fetch(`https://api.github.com/users/${username}`);
if (!response.ok) {
throw new Error(`HTTP error ${response.status}`);
}
const user = await response.json();
console.log(user.name);
} catch (error) {
console.error("Could not load user:", error.message);
} finally {
console.log("Finished the lookup.");
}
}
showUser("torvalds");The catch block runs for any failure inside the try — a dropped connection, a bad status you threw yourself, or invalid JSON. The optional finally block runs either way, which is handy for cleanup like hiding a loading spinner. This is the same mental model you use for synchronous errors, and that is a big part of why async/await feels so natural.
Common Gotchas and Our Recommendation
1. Forgetting await
If you leave off await, you get the Promise object itself instead of its value — a classic source of confusing [object Promise] bugs.
const pending = fetch(url); // a pending Promise, not the data
const response = await fetch(url); // the actual response2. await only works inside async
Using await in a regular function is a syntax error. It is allowed at the top level of an ES module, but inside ordinary functions you must mark them async first.
3. Accidentally making things slow
Awaiting in a loop runs tasks one after another. If they do not depend on each other, run them together with Promise.all:
// Slow: waits for each request before starting the next
const slowA = await fetch("/api/a");
const slowB = await fetch("/api/b");
// Fast: both requests run at the same time
const [fastA, fastB] = await Promise.all([
fetch("/api/a"),
fetch("/api/b")
]);Our recommendation
For everyday code, reach for async/await — it is the most readable option and makes error handling with try/catch straightforward. Keep then/catch in your toolkit for short one-liners, and use Promise.all whenever independent tasks can run in parallel. Remember that all three are the same Promises underneath, so you can mix them freely. To practise this with guided projects, work through our free JavaScript course, which covers Promises and async code in order from the basics.
Frequently Asked Questions
Is async/await better than Promises?
They are not really competitors — async/await is syntax built on top of Promises, so it is Promises with a friendlier face. For most code, async/await reads more clearly, but then/catch and Promise.all are still useful, and you can mix them freely in the same project.
Can I use await outside an async function?
Only at the top level of an ES module, where modern JavaScript allows it. Anywhere else, using await in a non-async function is a syntax error — just add the async keyword to the function first, then await inside it.
Why does my async function return a Promise instead of the value?
Because every async function returns a Promise by design. To get the actual value out, either await the function call from inside another async function, or attach .then() to it and read the value in the callback.
Does await freeze my whole web page?
No. await only pauses the async function it sits in. The rest of your JavaScript and the browser keep running, so the page stays responsive while the awaited task finishes in the background.
Why doesn't my fetch() catch a 404 error?
Because fetch only rejects on a network failure, not on HTTP error statuses like 404 or 500. You must check response.ok yourself and throw an error when it is false, so your surrounding try/catch can handle it.
