Promises and async/await confuse almost everyone at first. This guide builds the idea from a simple waiting problem, with diagrams and small examples you can run.
Sooner or later every JavaScript developer hits the same wall. You call a function, it goes off to fetch something from the network, and your next line runs before the answer comes back. The variable is empty, the page looks broken, and the code looks perfectly fine.
The fix is a promise, and the nicer way to write promises is async/await. Neither one is hard once you see what problem they solve.
By the end you will be able to:
- Explain what a promise is and what its three states mean.
- Chain steps with
.then()and handle errors with.catch(). - Rewrite promise code using
asyncandawait. - Run several tasks at once with
Promise.alland know when not to.
The problem: some things take time
Reading a file, calling an API and waiting for a timer are all slow compared with normal code. JavaScript does not stop and wait for them. It hands the slow job off, keeps running the rest of your code, and comes back when the result is ready.
That is great for keeping a page responsive. It also means you cannot write code that depends on the result in the next line, like this:
let user;
fetch("/api/user").then((res) => res.json()).then((data) => {
user = data;
});
console.log(user); // undefined, the request has not finished yetThe console.log runs first. The data arrives later. We need a way to say “do this after the answer shows up”.
What a promise really is
A promise is an object that stands in for a value you do not have yet. Think of the buzzer a restaurant gives you while you wait for a table. It is not your table, but it is a reliable way to find out when your table is ready.
Every promise is in exactly one of three states, and it only ever moves forward:
Once a promise is fulfilled or rejected it is settled, and it never changes again. That is what makes promises predictable.
Using a promise with then and catch
You attach what should happen next with .then(). If anything fails along the way, .catch() handles it. Each .then() returns a new promise, so you can chain steps in a straight line instead of nesting them.
fetch("/api/user")
.then((res) => res.json())
.then((user) => {
console.log("Hello", user.name);
return fetch("/api/orders?user=" + user.id);
})
.then((res) => res.json())
.then((orders) => console.log(orders.length, "orders"))
.catch((err) => console.error("Something failed:", err))
.finally(() => console.log("done"));Read it top to bottom like a to-do list. A single .catch() at the end catches a failure from any step above it, and .finally() runs whether things worked or not, which is handy for hiding a loading spinner.
The same code with async and await
async and await are a friendlier way to write the same thing. Mark a function async, and inside it you can put await in front of a promise. The function pauses at that line until the promise settles, then carries on with the value.
async function showOrders() {
try {
const res = await fetch("/api/user");
const user = await res.json();
const ordersRes = await fetch("/api/orders?user=" + user.id);
const orders = await ordersRes.json();
console.log(user.name, "has", orders.length, "orders");
} catch (err) {
console.error("Something failed:", err);
} finally {
console.log("done");
}
}
showOrders();It reads like normal top to bottom code, and errors use a normal try / catch. Only that one function waits. The rest of your program keeps running.
Doing several things at once
Awaiting one thing after another is the right call when each step needs the one before it. If the tasks are independent, waiting in a line wastes time. Start them together and wait for all of them:
async function loadDashboard() {
const [profile, orders, alerts] = await Promise.all([
fetch("/api/profile").then((r) => r.json()),
fetch("/api/orders").then((r) => r.json()),
fetch("/api/alerts").then((r) => r.json()),
]);
return { profile, orders, alerts };
}If each call takes one second, the sequential version takes about three seconds and this version takes about one. Here are the four helpers you will meet most often:
| Helper | Finishes when | If one fails |
|---|---|---|
Promise.all | Every promise is fulfilled | Rejects right away with that error |
Promise.allSettled | Every promise has settled | Never rejects, gives you each result |
Promise.race | The first one settles | Follows whichever settled first |
Promise.any | The first one is fulfilled | Rejects only if all of them fail |
Mistakes almost everyone makes
- **Forgetting
await.** You get a pending promise instead of the value, and strange output likePromise { <pending> }. - **Using
awaitinsideforEach.**forEachdoes not wait for async callbacks. Use afor...ofloop, orPromise.allwithmap. - Awaiting in a loop when you did not need to. If the calls are independent, run them together.
- Swallowing errors. An empty
catchhides bugs. At least log the error. - **Not checking
res.ok.**fetchonly rejects on network failure. A 404 or 500 still counts as a fulfilled promise.
const res = await fetch("/api/user");
if (!res.ok) {
throw new Error("Request failed with status " + res.status);
}
const user = await res.json();Try it yourself
Open your browser console and paste this. It wraps setTimeout in a promise so you can see await in action.
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
async function demo() {
console.log("start");
await wait(1000);
console.log("one second later");
}
demo();
console.log("this prints before the second line");Predict the order of the three messages before you run it. Then change wait(1000) to start two waits with Promise.all and time it.