JavaScript Promises and async/await Explained with Examples

JavaScript Promises and async/await Explained with Examples

Sooner or later every JavaScript developer writes code like this, and is surprised by what comes out:

JavaScript: what prints first?
console.log("A");
setTimeout(() => console.log("B"), 0);
Promise.resolve().then(() => console.log("C"));
console.log("D");

The answer is A, D, C, B. B has a delay of zero and still prints last. By the end of this guide you'll know exactly why, and you'll be able to use Promises and async/await with confidence.

Open the event loop visualizer Step through this code one moment at a time.

The short version

TermWhat it means
SynchronousRuns now, one line at a time
AsynchronousStarts now, finishes later, and other code runs in between
PromiseAn object standing for a value you will get later
asyncMakes a function always return a Promise
awaitPauses this function until a Promise settles
Event loopDecides which waiting code runs next

Why JavaScript needs this

JavaScript runs one thing at a time on a single thread. If it stopped and waited two seconds for a server, the whole page would freeze for two seconds: no scrolling, no clicks, nothing.

So slow jobs are handed to the browser instead. JavaScript says “fetch this, and let me know when you're done”, then carries on. The hard part is writing code that continues when the answer arrives, and that's what Promises are for.

What is a Promise?

Think of a coffee shop. You pay and get a receipt, not a coffee. The receipt is a Promise: it stands for something you'll have later. You can sit down and do other things while you wait. Later you either get your coffee, or they tell you they've run out.

A Promise is always in one of three states:

Once a Promise is fulfilled or rejected it is settled, and it never changes again. You'll mostly use Promises that other code gives you, such as fetch(), but here is how to create one:

JavaScript: creating a Promise
function wait(ms) {
  return new Promise((resolve, reject) => {
    if (ms < 0) {
      reject(new Error("Time can't be negative"));
      return;
    }
    setTimeout(() => resolve("Waited " + ms + "ms"), ms);
  });
}

You call resolve when the work succeeds and reject when it fails.

Using a Promise with then and catch

JavaScript: then, catch, finally
wait(1000)
  .then((message) => {
    console.log(message);          // "Waited 1000ms"
    return message.length;         // goes to the next then
  })
  .then((length) => console.log(length))   // 13
  .catch((error) => console.log(error.message))
  .finally(() => console.log("Finished"));

then runs when the Promise is fulfilled, catch runs when anything above it fails, and finally runs either way. Each then returns a new Promise, which is why you can chain them.

async/await: the same thing, easier to read

async and await let you write Promise code that reads from top to bottom, like normal code. These two functions do the same job:

JavaScript: then vs await
// With then
function showUser() {
  return fetchUser()
    .then((user) => fetchOrders(user.id))
    .then((orders) => console.log(orders));
}

// With async/await
async function showUser() {
  const user = await fetchUser();
  const orders = await fetchOrders(user.id);
  console.log(orders);
}

There are two rules to remember:

  1. async makes a function return a Promise. Even return 5 gives you a Promise that resolves to 5.
  2. await waits for a Promise and gives you its value. It can only be used inside an async function, or at the top level of a module.

What await really does

The visualizer paused on the line const user = await loadUser. The call stack holds only the script, a 1000ms timer is waiting in Web APIs, and the console shows lines 1 and 2
At await, getUser leaves the call stack. The script carries on while the timer runs.

Here is the most important idea in this guide: await does not stop your program. It only pauses the function it is in. The function steps off the call stack, and everything else keeps running.

JavaScript: the order of events
async function getUser() {
  console.log("2: getUser starts");
  const user = await loadUser();
  console.log("4: got " + user);
}

function loadUser() {
  return new Promise((resolve) =>
    setTimeout(() => resolve("Aisha"), 1000));
}

console.log("1: before");
getUser();
console.log("3: after");

The output is 1, 2, 3, 4. Notice that “3: after” prints before the user has loaded. The code before the first await runs straight away, then getUser pauses and the script moves on. Step through this example to watch it happen.

The event loop

The visualizer after the script has ended: the call stack is empty, the console shows A and D, the microtask queue holds log C and the task queue holds log B
The script has finished. C and B are both ready, and the microtask queue goes first.

The event loop explains the A, D, C, B puzzle. JavaScript has one call stack, where code runs, and two waiting lines:

The event loop follows three simple rules:

  1. Run the current code until the call stack is empty.
  2. Run all the microtasks, including any new ones they add.
  3. Take one task, run it, and go back to step 2.

Now the puzzle is easy. A and D are normal code, so they print first. Then the stack is empty, so the microtask (C) runs. Only then does the task (B) get a turn. A delay of 0 means “as soon as possible after everything else”, not “right now”. See every step, or test yourself on the harder interview question.

The visualizer one second later: the timer callback is on the call stack and resume getUser is waiting in the microtask queue
When the Promise resolves, the rest of getUser joins the microtask queue.

Real example: fetch data from an API

This is the code you'll write most often. It's worth learning as a pattern:

JavaScript: fetch with error handling
async function getUser(id) {
  const response = await fetch(`https://api.example.com/users/${id}`);

  if (!response.ok) {
    throw new Error(`Request failed: ${response.status}`);
  }

  return response.json();
}

async function showUser(id) {
  try {
    const user = await getUser(id);
    console.log(user.name);
  } catch (error) {
    console.log("Could not load the user:", error.message);
  } finally {
    console.log("Hide the loading spinner here");
  }
}

Two things here surprise people:

Handling errors with try/catch

With await, a rejected Promise throws an error at that line, so a normal try/catch handles it. An error thrown inside an async function rejects the Promise it returned, so errors travel up to whichever caller catches them.

If nobody catches a rejection, the browser reports an unhandled rejection in the console. Always have a try/catch or a .catch() at the top of the chain.

One at a time vs all at once

A timeline of three requests run in a loop: User runs from 0 to 1.2 seconds, Orders from 1.2 to 2 seconds, and Reviews from 2 to 4 seconds, finishing in 4.0 seconds
await in a loop runs one request at a time. The total is 4.0 seconds.

This is the most common performance mistake with await. If each request waits for the one before it, the times add up:

JavaScript: slow, one at a time
const user = await fetchUser();        // 1.2s
const orders = await fetchOrders();    // 0.8s
const reviews = await fetchReviews();  // 2.0s
// total: 4.0s

When the requests don't depend on each other, start them together with Promise.all. The total is then only as long as the slowest one:

JavaScript: fast, all at once
const [user, orders, reviews] = await Promise.all([
  fetchUser(),
  fetchOrders(),
  fetchReviews()
]);
// total: 2.0s
A timeline of the same three requests all starting at 0 seconds, finishing together in 2.0 seconds
Promise.all starts everything at once. The total is 2.0 seconds, the time of the slowest request.

Keep them in sequence only when one result is needed for the next call, such as when you need the user's id to load their orders.

When one request fails

A timeline where Orders fails at 0.8 seconds in red, while User and Reviews continue to finish. The message says the error was caught after 0.8 seconds
Promise.all rejects on the first failure, though the other requests keep running.

Each helper has a different rule for failure:

MethodFinishes whenUse it when
Promise.allAll succeed, or the first one failsYou need every result
Promise.allSettledAll have finished, good or badPartial results are fine
Promise.raceThe first one finishes, good or badAdding a timeout
Promise.anyThe first one succeedsTrying several sources
JavaScript: Promise.allSettled
const results = await Promise.allSettled([fetchUser(), fetchOrders()]);

for (const result of results) {
  if (result.status === "fulfilled") {
    console.log(result.value);
  } else {
    console.log("Failed:", result.reason.message);
  }
}

A page with several separate widgets is a good fit: if the reviews fail to load, you can still show the rest.

Common mistakes

JavaScript: fixing await in a loop
// Wrong: the log runs before any of these finish
items.forEach(async (item) => {
  await save(item);
});
console.log("Done?");

// Right: one at a time
for (const item of items) {
  await save(item);
}
console.log("Done");

// Right: all at once
await Promise.all(items.map((item) => save(item)));
console.log("Done");

Try it in the visualizer

The event loop visualizer with code on the left, boxes for the call stack, Web APIs, microtask queue and task queue, and a console showing A, D and C
Step through real code and watch each callback wait its turn.

The event loop visualizer has two parts. The stepper shows the call stack, both queues and the console, with a plain explanation of each moment. The timeline below it runs real Promises so you can compare the patterns. Try these:

Share your code

Stuck on an async bug? Share your code with the free code sharing tool, or share the error message as a link. No account needed.

Frequently asked questions

What is the difference between a Promise and async/await?

A Promise is an object that stands for a value you will get later. async/await is a cleaner way to write code that uses Promises. An async function always returns a Promise, and await waits for one, so they are the same system with different syntax.

Does await block the whole page?

No. await only pauses the async function it is inside. The function comes off the call stack, so the rest of your code keeps running and the page stays responsive. The function continues when the Promise settles.

Why does my async function return a Promise instead of the value?

An async function always returns a Promise, even when you return a plain value. To get the value, use await in another async function, or call .then() on the result.

Why does setTimeout with 0ms run after a Promise?

Promise callbacks go into the microtask queue and setTimeout callbacks go into the task queue. When the call stack is empty, the event loop empties the whole microtask queue before it takes the next task, so the Promise goes first.

Does fetch throw an error on a 404?

No. fetch only rejects when the request itself fails, such as a network error. A 404 or 500 is still a completed response, so check response.ok and throw your own error if it is false.

Can I use await outside an async function?

Only at the top level of a JavaScript module, meaning a file loaded with type="module". In a normal script or a regular function it is a syntax error.

What is the difference between Promise.all and Promise.allSettled?

Promise.all rejects as soon as one Promise fails, and you lose the other results. Promise.allSettled waits for every Promise to finish and gives you a list showing which succeeded and which failed.

Related articles

← All guides