Back to React
React
medium
mid

Does React use Promise.allSettled for parallel API calls, and how would that work internally?

No — React itself doesn't make API calls. The developer does, via fetch/axios/React Query. To parallelize, the developer uses Promise.all (fails fast) or Promise.allSettled (resolves to per-promise results regardless of failures). React Query handles parallelism via independent useQuery hooks that fire concurrently. Suspense + use() enables waterfall avoidance via parallel data dependencies.

7 min read·~5 min to think through

Common misconception cleared up first.

React doesn't fetch

React renders. The developer chooses how to fetch. There's no built-in Promise.all behavior baked into React.

Promise.all vs Promise.allSettled

js
// Promise.all — rejects on first failure
const [a, b, c] = await Promise.all([
  fetch('/a'), fetch('/b'), fetch('/c'),
]);

// Promise.allSettled — always resolves with an array of results
const results = await Promise.allSettled([
  fetch('/a'), fetch('/b'), fetch('/c'),
]);

results.forEach(r => {
  if (r.status === 'fulfilled') console.log(r.value);
  else console.error(r.reason);
});
  • Promise.all is right when you need ALL responses to proceed (a transaction).
  • Promise.allSettled is right when partial success is acceptable (a dashboard).

Both run the requests in parallel — the difference is how rejections compose.

In React

tsx
function Dashboard() {
  // Three independent queries fire in parallel — React Query handles parallelism.
  const users = useQuery({ queryKey: ['users'], queryFn: fetchUsers });
  const orders = useQuery({ queryKey: ['orders'], queryFn: fetchOrders });
  const metrics = useQuery({ queryKey: ['metrics'], queryFn: fetchMetrics });

  if (users.isLoading || orders.isLoading || metrics.isLoading) return <Spinner />;
  return <Layout u={users.data} o={orders.data} m={metrics.data} />;
}

Each useQuery fires its request immediately on mount. Three are in flight at once.

If you want allSettled semantics:

tsx
// React Query useQueries
const results = useQueries({
  queries: [
    { queryKey: ['users'], queryFn: fetchUsers },
    { queryKey: ['orders'], queryFn: fetchOrders },
    { queryKey: ['metrics'], queryFn: fetchMetrics },
  ],
});

const anyError = results.some(r => r.isError);
const anyLoading = results.some(r => r.isLoading);

Suspense + use()

React 19's use(promise) integrates Suspense with promises. With parallel fetching:

tsx
function Page({ usersPromise, ordersPromise }: Props) {
  // Both promises started before the render — parallel.
  const users = use(usersPromise);
  const orders = use(ordersPromise);
  return <Layout users={users} orders={orders} />;
}

// caller
const usersPromise = fetchUsers();   // started immediately
const ordersPromise = fetchOrders(); // started immediately, in parallel
<Suspense fallback={<Spinner />}>
  <Page usersPromise={usersPromise} ordersPromise={ordersPromise} />
</Suspense>

The key is starting the promises before the component renders — otherwise you get a waterfall.

Server Components + parallel fetches

tsx
// Server component
async function Page() {
  const [users, orders] = await Promise.all([fetchUsers(), fetchOrders()]);
  return <Layout users={users} orders={orders} />;
}

The two fetches run in parallel on the server; the client receives the merged result.

Anti-patterns

  • Waterfall fetching: each component fetches in useEffect using the previous result.
  • Fix: lift fetches up, fetch in parallel, pass results down.
  • await inside loops when items are independent:

``js // Bad for (const id of ids) await fetch(id); // Good await Promise.all(ids.map(id => fetch(id))); ``

  • Promise.all when partial failure is fine: rejects the whole batch on one bad call.

When to use which

NeedChoose
All must succeed (transaction)Promise.all
Show partial dataPromise.allSettled
First responder wins (with cancel)Promise.race
First successful (skip failures)Promise.any
Many concurrent queries in ReactuseQueries
Server-side parallel dataRSC + Promise.all

How parallelism works under the hood

The event loop kicks off all promises. fetch creates network requests that the browser sends concurrently (subject to HTTP/2 multiplexing or HTTP/1.1 connection limits — typically 6 per origin). All resolve into microtasks as their responses arrive. The aggregator (Promise.all etc.) collects them and resolves the outer promise.

There's no thread-level parallelism — it's I/O concurrency. The CPU work for parsing each response is still serialized on the main thread.

Follow-up questions

  • How does useQueries differ from multiple useQuery calls?
  • What's a waterfall fetch and how do you avoid it?
  • When would Promise.any beat Promise.all?

Common mistakes

  • Assuming React magically parallelizes — it doesn't; the developer composes promises.
  • Sequential await in a loop when items are independent.
  • Promise.all when partial failure should still show data.

Performance considerations

  • Parallelism cuts wall-clock time to max(durations) instead of sum(durations). Big win for independent fetches. HTTP/2 multiplexes; HTTP/1.1 caps. Server-side fetches are usually faster than client-side (closer to origin, no auth handshake on every request).

Edge cases

  • HTTP/1.1 limits to 6 connections per origin — beyond that requests queue.
  • AbortController to cancel in-flight when no longer needed.
  • Memory: many huge response bodies in flight can OOM low-memory devices.

Real-world examples

  • Dashboards (users + metrics + activity), product pages (item + reviews + recommendations), settings pages (profile + preferences + integrations). All benefit from parallel fetching.

Senior engineer discussion

Senior framing: parallelism is about latency, not throughput. Start independent requests as early as possible, prefer Promise.all for required data and allSettled for optional. React Query's parallel hooks model is the practical answer; Suspense + use() is the future direction.

Related questions