On this page

Promises & async/await

4 min read

Every API call in a React app returns a Promise. Knowing how promises and async/await really behave is the difference between a clean data layer and a pile of unhandled errors.

Definition

A Promise is an object that stands for a value that is not ready yet. It is always in one of three states:

1pending → work is still running
2fulfilled → finished with a value
3rejected → failed with an error

Once a promise is fulfilled or rejected it is settled, and it never changes again.

Analogy: a restaurant buzzer. You get it right away (the promise). Later it either lights up with your food (fulfilled) or tells you the kitchen ran out (rejected).

Using Promises

js
1fetch("/api/user")
2 .then((res) => res.json()) // runs on success, returns a new promise
3 .then((user) => console.log(user))
4 .catch((err) => console.error(err)) // runs if ANY step above failed
5 .finally(() => console.log("done")); // runs either way

Each .then returns a new promise, which is why you can chain them. Returning a value passes it to the next .then. Returning a promise makes the chain wait for it. A thrown error skips to the nearest .catch.

async / await

async/await is just a cleaner way to write the same promises.

js
1async function loadUser() {
2 try {
3 const res = await fetch("/api/user");
4 if (!res.ok) throw new Error("HTTP " + res.status);
5 return await res.json();
6 } catch (err) {
7 console.error(err);
8 } finally {
9 console.log("done");
10 }
11}

Rules to remember:

  • An async function always returns a promise, even if you return a plain value.
  • await pauses only that function, not the whole program. The rest of the code keeps running, and the code after await resumes as a microtask.
  • await can only be used inside an async function, or at the top level of an ES module.

Gotcha 1: fetch Does Not Reject on HTTP Errors

Here is what breaks. A 404 or 500 response is still a successful fetch. The promise only rejects on network failure.

js
1const res = await fetch("/api/missing"); // resolves, even with status 404
2if (!res.ok) throw new Error("Request failed: " + res.status); // you must check

Gotcha 2: Sequential vs Parallel

js
1// Slow: second request waits for the first (about 2 seconds total)
2const a = await getA();
3const b = await getB();
4
5// Fast: both start together (about 1 second total)
6const [a, b] = await Promise.all([getA(), getB()]);

If the two calls do not depend on each other, start them together.

Gotcha 3: await in a Loop and forEach

js
1items.forEach(async (item) => {
2 await save(item); // ✗ forEach does not wait, the loop finishes immediately
3});
4
5for (const item of items) {
6 await save(item); // ✓ runs one by one, in order
7}
8
9await Promise.all(items.map(save)); // ✓ runs all at once

Gotcha 4: Unhandled Rejections

A rejected promise with no .catch and no try/catch causes an unhandled rejection. It can crash a Node process or show a console error in the browser. Always handle errors at the point where you can do something about them.

In React

You cannot make a useEffect callback itself async, because effects must return nothing or a cleanup function, not a promise.

jsx
1useEffect(() => {
2 async function load() {
3 const res = await fetch("/api/user");
4 setUser(await res.json());
5 }
6 load(); // ✓ define an async function inside and call it
7}, []);

Also guard against stale responses and unmounted components using an AbortController or an ignore flag, which we cover in the data-fetching section. In practice, a library like TanStack Query handles most of this for you.

Summary

1Promise → a value that will exist later: pending, fulfilled or rejected
2then / catch / finally → chain steps, errors skip to the nearest catch
3async function → always returns a promise
4await → pauses that function only, resumes as a microtask
5fetch → rejects on network failure only, check res.ok yourself
6Independent calls → Promise.all, not one await after another
7forEach with await → does not wait, use for...of or Promise.all
8useEffect → define an inner async function, never make the callback async

When you have several promises at once, you need to choose how to combine them. That is next.

Next TopicPromise.all, allSettled, race