Promise.all in the Next.js App Router
By Drew Bredvick··Updated ·2 min read
Welcome to App Router World — everything is nice and new and shiny here.
This isn't one of those recipe websites so I'll skip the preable and start with the valuable insight:
If you find yourself reaching for Promise.all in RSCs, you probably want Suspense instead.
If you already understand that, skip the rest of the post. Otherwise, here's how I ran into this issue and came up with this rule of thumb.
I was recently working on a Next.js app that needed to fetch from a bunch of data sources. React Server Components make this easy.

In the world of getServerSideProps, I would have written something like this:
export async function getServerSideProps() {
const [data1, data2, data3] = await Promise.all([
fetch('https://api1.com'),
fetch('https://api2.com'),
fetch('https://api3.com'),
])
return {
props: {
data1,
data2,
data3,
},
}
}
Promise.all makes all my API calls in parallel and returns an array of the results. Works pretty well as long as none of my API calls are slow and works even better if I refactor it to use Incremental Static Regeneration (ISR).
In RSC land, I don't have getServerSideProps. Instead, I can just fetch in my component:
export default async function MyPage() {
const [data1, data2, data3] = await Promise.all([
fetch('https://api1.com'),
fetch('https://api2.com'),
fetch('https://api3.com'),
])
return (
<div>
{data1 && <div>{data1}</div>}
{data2 && <div>{data2}</div>}
{data3 && <div>{data3}</div>}
</div>
)
}
Pretty cool right? But what happens if one of my API calls is slow? Well, the whole component will be slow to render.
If you've been following Next.js closely, you might ask:
But I thought Next.js App Router implemented streaming? Shouldn't this be fine?
And you'd be right, if we had remembered to use Suspense instead of Promise.all.
import { Suspense } from 'react';
async function Data1() {
const res = await fetch('https://api1.com');
const data = await res.json();
return <div>{data}</div>;
}
async function Data2() {
const res = await fetch('https://api2.com');
const data = await res.json();
return <div>{data}</div>;
}
async function Data3() {
const res = await fetch('https://api3.com');
const data = await res.json();
return <div>{data}</div>;
}
export default function MyPage() {
return (
<div>
<Suspense fallback={<div>Loading...</div>}>
<Data1 />
</Suspense>
<Suspense fallback={<div>Loading...</div>}>
<Data2 />
</Suspense>
<Suspense fallback={<div>Loading...</div>}>
<Data3 />
</Suspense>
</div>
)
}
Now this snippet does what we want. As the data is available, it will render. If a particular API call is slow, it will show the fallback until the data is available.
If you're wanting to create a page-level fallback, you can use the loading.js file convention described in the Next.js docs.
So again, if you find yourself reaching for Promise.all in RSCs, you probably want Suspense instead.
If you want to level this up, Cache Components let Next.js prerender a static shell, stream dynamic work through Suspense, and include cached work in the shell with use cache.
Continue reading
A few useful places to go next.
- Why Next.js?Next.js is my default choice when starting a new project, big or small. Here's why I think you should use it, pros and cons included.
- Ecom ManifestoEcommerce is hard. Here's a path to making it easier, at least technically.
- How I pick my tech stack for side projectsEither pick new tech to learn or use tech you know to build fast.

The newsletter
Don’t miss the next one.
Field notes on GTM engineering and the craft of shipping software in the AI era — straight to your inbox.
No spam. Unsubscribe anytime.