React
Async Await
JavaScript
Asynchronous Programming
Web Development

How to await multiple async await requests in React

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

In React, waiting for multiple asynchronous requests is mostly a JavaScript problem with one React-specific concern: component lifecycle. If requests are independent, start them together with Promise.all. If one request depends on another, await them in sequence and clean up correctly so stale responses do not update unmounted components.

Run Independent Requests in Parallel

If two requests do not depend on each other, do not await the first before starting the second. Launch both immediately and await them together.

jsx
1import { useEffect, useState } from "react";
2
3export default function Dashboard() {
4  const [data, setData] = useState(null);
5  const [error, setError] = useState(null);
6
7  useEffect(() => {
8    let cancelled = false;
9
10    async function load() {
11      try {
12        const [userResponse, statsResponse] = await Promise.all([
13          fetch("/api/user"),
14          fetch("/api/stats"),
15        ]);
16
17        const [user, stats] = await Promise.all([
18          userResponse.json(),
19          statsResponse.json(),
20        ]);
21
22        if (!cancelled) {
23          setData({ user, stats });
24        }
25      } catch (err) {
26        if (!cancelled) {
27          setError(err instanceof Error ? err.message : String(err));
28        }
29      }
30    }
31
32    load();
33    return () => {
34      cancelled = true;
35    };
36  }, []);
37
38  if (error) return <div>Error: {error}</div>;
39  if (!data) return <div>Loading...</div>;
40  return <pre>{JSON.stringify(data, null, 2)}</pre>;
41}

This is the normal answer for dashboards and pages that need several unrelated resources before rendering.

Use Sequential await Only for Real Dependencies

Sometimes the second request cannot start until the first one finishes. In that case, sequential code is correct.

jsx
1useEffect(() => {
2  let cancelled = false;
3
4  async function load() {
5    try {
6      const userResponse = await fetch("/api/user");
7      const user = await userResponse.json();
8
9      const ordersResponse = await fetch(`/api/orders?userId=${user.id}`);
10      const orders = await ordersResponse.json();
11
12      if (!cancelled) {
13        console.log(user, orders);
14      }
15    } catch (err) {
16      if (!cancelled) {
17        console.error(err);
18      }
19    }
20  }
21
22  load();
23  return () => {
24    cancelled = true;
25  };
26}, []);

The rule is simple: sequence only when the second request needs output from the first.

Handle Partial Failure With Promise.allSettled

Promise.all fails fast. That is good when the screen is useless unless every request succeeds. It is the wrong tool when partial data is still valuable.

jsx
1async function loadWidgets() {
2  const results = await Promise.allSettled([
3    fetch("/api/weather").then((r) => r.json()),
4    fetch("/api/news").then((r) => r.json()),
5    fetch("/api/alerts").then((r) => r.json()),
6  ]);
7
8  return results.map((result) =>
9    result.status === "fulfilled" ? result.value : null
10  );
11}

This is useful for composite pages where one failed panel should not blank the whole screen.

Clean Up In-Flight Work

The most common React-specific bug is letting a request finish after the component unmounts or after newer props triggered a different request. The safest default is AbortController.

jsx
1import { useEffect, useState } from "react";
2
3function Profile({ userId }) {
4  const [profile, setProfile] = useState(null);
5
6  useEffect(() => {
7    const controller = new AbortController();
8
9    async function load() {
10      try {
11        const response = await fetch(`/api/profile/${userId}`, {
12          signal: controller.signal,
13        });
14        const json = await response.json();
15        setProfile(json);
16      } catch (err) {
17        if (!(err instanceof DOMException && err.name === "AbortError")) {
18          console.error(err);
19        }
20      }
21    }
22
23    load();
24    return () => controller.abort();
25  }, [userId]);
26
27  return <div>{profile ? profile.name : "Loading..."}</div>;
28}

This matters even more when props change quickly and older responses could race with newer ones.

Keep Async Logic Out of the Effect Signature

useEffect itself should not be declared async. Instead, define an inner async function and call it. That keeps cleanup behavior compatible with React’s expectations.

jsx
1useEffect(() => {
2  async function load() {
3    // async work here
4  }
5
6  load();
7}, []);

That pattern is standard because effect callbacks must return either nothing or a cleanup function, not a promise.

Common Pitfalls

A common mistake is awaiting independent requests one by one and making the UI slower than necessary. Another is using Promise.all where one failure should not cancel everything.

It is also easy to forget cleanup, which leads to stale updates, warnings, or subtle race conditions after prop changes. Finally, do not blame React for ordinary promise orchestration bugs. The concurrency model is still JavaScript promises plus component lifecycle management.

Summary

  • Use Promise.all when multiple requests are independent and all are required.
  • Use sequential await only when later requests depend on earlier results.
  • Use Promise.allSettled when partial success is still useful.
  • Put async work inside the effect, not on the effect callback itself.
  • Abort or ignore stale requests so old responses do not update the wrong component state.

Course illustration
Course illustration

All Rights Reserved.