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 async and await.
  • Run several tasks at once with Promise.all and 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:

broken.js
let user;
fetch("/api/user").then((res) => res.json()).then((data) => {
  user = data;
});

console.log(user); // undefined, the request has not finished yet

The console.log runs first. The data arrives later. We need a way to say “do this after the answer shows up”.

Your code does not pause and wait. It keeps running other lines, and the answer arrives later.

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:

A promise starts as pending, then settles once, either fulfilled or rejected.

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.

chain.js
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.js
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.

Same behavior, two writing styles. Most teams prefer the one on the right for everyday code.

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:

parallel.js
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:

HelperFinishes whenIf one fails
Promise.allEvery promise is fulfilledRejects right away with that error
Promise.allSettledEvery promise has settledNever rejects, gives you each result
Promise.raceThe first one settlesFollows whichever settled first
Promise.anyThe first one is fulfilledRejects only if all of them fail

Mistakes almost everyone makes

  • **Forgetting await.** You get a pending promise instead of the value, and strange output like Promise { <pending> }.
  • **Using await inside forEach.** forEach does not wait for async callbacks. Use a for...of loop, or Promise.all with map.
  • Awaiting in a loop when you did not need to. If the calls are independent, run them together.
  • Swallowing errors. An empty catch hides bugs. At least log the error.
  • **Not checking res.ok.** fetch only rejects on network failure. A 404 or 500 still counts as a fulfilled promise.
check-ok.js
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.

practice.js
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.