What you'll be able to do

By the end of this page you should be able to start independent requests together with Promise.all, know when allSettled is safer, and recognize a waterfall of awaits that could have been parallel.

Previous post: HTTP status and error handling.

Who this is for

  • People who await fetch A then await fetch B when A and B do not depend on each other
  • Beginners who need one failure to cancel the whole batch — or not
  • Anyone preparing for dashboard-style pages that load several resources

You can skip this if parallel helpers are already clear. Come back when a page feels slow for no good reason.

Slow waterfall vs parallel

// Waterfall — total ≈ timeA + timeB
const a = await fetchJson("/api/a");
const b = await fetchJson("/api/b");

// Parallel — total ≈ max(timeA, timeB)
const [a2, b2] = await Promise.all([
  fetchJson("/api/a"),
  fetchJson("/api/b"),
]);

Scenario A — user profile + settings. Independent → Promise.all.

Scenario B — step 2 needs step 1’s id. Keep sequential awaits on purpose.

Promise.all

  • Fulfills when all fulfill (results in order).
  • Rejects immediately if any rejects (others may still finish in the background).

Scenario A — all required. Fail the screen if one critical call fails.

Scenario B — one optional widget. Prefer allSettled so one failure does not kill everything.

Promise.allSettled

const results = await Promise.allSettled([p1, p2]);
// [{ status: "fulfilled", value }, { status: "rejected", reason }]

Scenario A — dashboard widgets. Show what you can; mark failures.

Scenario B — analytics + main data. Main data required; analytics optional.

Promise.race and Promise.any (short)

await Promise.race([fetchJson(url), timeout(3000)]);

race — first to settle (fulfill or reject) wins.
any — first to fulfill wins; rejects only if all reject.

Scenario A — soft timeout pattern with race (build a rejecting timer carefully).

Scenario B — multiple mirrors. any can take the first successful mirror.

A tiny practice file

<!DOCTYPE html>
<html>
  <body>
    <button id="go">Load two todos</button>
    <pre id="out"></pre>
    <script>
      async function fetchJson(url) {
        const res = await fetch(url);
        if (!res.ok) throw new Error("HTTP " + res.status);
        return res.json();
      }
      document.getElementById("go").addEventListener("click", async function () {
        const out = document.getElementById("out");
        out.textContent = "loading…";
        try {
          const [t1, t2] = await Promise.all([
            fetchJson("https://jsonplaceholder.typicode.com/todos/1"),
            fetchJson("https://jsonplaceholder.typicode.com/todos/2"),
          ]);
          out.textContent = t1.title + "\n" + t2.title;
        } catch (err) {
          out.textContent = err.message;
        }
      });
    </script>
  </body>
</html>

Mistakes I see a lot

1. Parallelizing dependent steps. Wrong order bugs.

2. Using all when one failure should be soft. Use allSettled.

3. Forgetting that all rejects on first error. Partial results are lost unless you settled.

4. Micro-optimizing three tiny local promises. Clarity first.

What to try before the next post

  1. Time waterfall vs Promise.all with two delays.
  2. Make one promise reject; compare all vs allSettled.
  3. Build the tiny HTML file.

Next in this series: AbortController with fetch — cancel in-flight requests when the user types again.

Try this next outside the series

Async code gets much easier when you see the same ideas on the browser side and the server side.