Data fetching patterns in React: waterfalls, parallelism and caching
Most slow React pages are not slow because of rendering. They are slow because six requests are queued behind each other for no reason.
Place error boundaries around independently recoverable regions rather than the whole app, use Suspense with skeletons that match the eventual layout, and always give the user a way forward — a retry action, cached content, or a clear next step. A failure that takes down the entire page is a design choice, not an inevitability.
Around each region that can fail independently and still leave a usable page.
A dashboard with six widgets should have six boundaries, not one. If the revenue chart's API is down, the other five widgets should still render and the chart should show a compact error with a retry button.
In the Next.js App Router this is built into routing: an error.tsx file scopes a boundary to its route segment, and a nested segment's error keeps the parent layout intact. Put error.tsx where the recoverable unit is.
// app/dashboard/reports/error.tsx
"use client";
export default function Error({ error, reset }: { error: Error; reset: () => void }) {
useEffect(() => { captureException(error); }, [error]);
return (
<div role="alert">
<h2>We could not load your reports</h2>
<p>This is usually temporary.</p>
<button onClick={reset}>Try again</button>
</div>
);
}| Error type | Caught by boundary? | Handle with |
|---|---|---|
| Render-time throw | Yes | Error boundary |
| Event handler throw | No | try/catch in the handler |
| Async rejection (fetch) | No | Catch and set error state |
| Server-side render error | Partly | Framework error handling |
| Errors inside the boundary itself | No | Keep fallbacks trivially simple |
This is why 'we have an error boundary' is not an error strategy. Most production errors are asynchronous, and they need explicit handling at the call site.
Not directly — the boundary itself must be a class component or come from a library like react-error-boundary. In the Next.js App Router, error.tsx wraps this for you.
Show a human summary, not the raw message. Raw errors can leak implementation details and are rarely actionable. Log the full detail server-side against a reference id.
Skeletons for content whose shape you know, spinners for indeterminate actions like a form submission. Skeletons reduce perceived wait because the page appears to be assembling.
Suspense handles the pending state, error boundaries handle the failed state. Most data-loading regions want both, and in the App Router loading.tsx and error.tsx give you each.
Harshal Patel
Founder & Lead Engineer, ROVQIX
Harshal leads engineering at ROVQIX, where he has shipped production Next.js, Node.js and PostgreSQL systems for startups, SaaS teams and ecommerce brands. He writes about the trade-offs behind architecture decisions rather than the framework of the week.
ROVQIXdesigns and builds production web platforms — Next.js front ends, Node.js APIs and the infrastructure behind them. Tell us what you're building and we'll scope it with you.
Most slow React pages are not slow because of rendering. They are slow because six requests are queued behind each other for no reason.
CLS is the most fixable Core Web Vital. Almost every point of it comes from an element that did not tell the browser how big it would be.
Observability is not more dashboards. It is being able to answer a question you did not anticipate, without shipping new code.
No spam. Just the occasional case study and craft breakdown.