On this page
A dashboard should remain useful when its activity feed is slow or its recommendations fail. React gives these situations different mechanisms: Suspense coordinates supported pending work, while error boundaries replace a subtree after a rendering failure. Treating both as one generic loading flag produces confusing recovery paths and unnecessarily empty screens.
Start with independent regions
Sketch the page as regions users can understand independently: account summary, recent invoices, and optional recommendations. Give each region a clear pending appearance and a failure message. Use the contrast checker to verify that muted skeleton labels, retry buttons, and error copy remain readable against their actual backgrounds. Loading states are still interface states, not decorative exceptions.
Choose boundary placement according to tasks rather than component count. An invoice form and its validation summary usually belong together; a marketing recommendation beside that form should not be able to replace it. Keep navigation and the page heading outside optional regions so users retain their location when something breaks.
Write down what remains usable during each failure. This simple exercise reveals where a full-page fallback is inappropriate. For broader layout decisions, the development hub provides a useful starting point, but resilience begins with product-specific expectations: can someone still download an existing invoice when recommendations are unavailable?
Distinguish pending work from errors
Suspense is not a fetch detector. A request started inside an ordinary effect does not automatically activate the nearest Suspense boundary. Supported sources include lazy component imports and integrations that explicitly support suspending reads. Follow the React Suspense documentation and your framework or data library rather than inventing a promise cache during render.
An error boundary catches errors thrown while rendering descendants and in supported React lifecycle work. It does not generally catch event-handler exceptions, arbitrary asynchronous callbacks, server rendering errors, or errors in its own fallback. A failed save initiated by a click therefore needs explicit handling in the mutation flow, often as a form-level message.
A request failure can reach an error boundary when a Suspense-aware data layer throws that error during render. That is an integration behavior, not a universal property of fetch. Likewise, a missing record might deserve a dedicated not-found screen rather than the same recovery message used for a temporary service outage.
Use expected failures as data whenever possible. Invalid input, permission-denied responses, and declined payments usually need specific instructions. Unexpected rendering failures belong in a containment mechanism with safe diagnostics and a reasonable escape route.
Compose boundaries around meaningful content
The following example uses the third-party react-error-boundary package and React's lazy loading. The lazy component import supplies a real Suspense source, making the example independent of any particular data-fetching library.
import { lazy, Suspense } from "react";
import { ErrorBoundary } from "react-error-boundary";
const ActivityFeed = lazy(() => import("./ActivityFeed"));
export function ActivityRegion() {
return (
<section aria-labelledby="activity-title">
<h2 id="activity-title">Recent activity</h2>
<ErrorBoundary
fallbackRender={() => (
<p>
Activity could not load. Save your work, then reload
the page to try again.
</p>
)}
>
<Suspense fallback={<p role="status">Loading activity…</p>}>
<ActivityFeed />
</Suspense>
</ErrorBoundary>
</section>
);
}Here, an import that is still pending shows the loading message; a rejected import reaches the error boundary. The heading stays visible in either case. A production implementation can add telemetry through the boundary's error callback, provided it removes credentials and personal data before transmission.
Avoid promising that resetting this boundary will retry a rejected lazy import. React caches the lazy loader's promise and result. Recovery may require a page reload or deployment-aware asset recovery, and reloading without warning can destroy unsaved work. Read React's lazy reference before building a universal retry button.
Preserve continuity while loading
A large boundary can coordinate a region that must appear together, while nested boundaries can reveal independent sections progressively. Neither arrangement is inherently better. Choose based on whether partial content helps people act or instead creates misleading combinations of old and new information.
For subsequent navigation, transitions can help preserve already revealed content instead of immediately replacing it with a fallback. They do not eliminate pending work, and their behavior depends on the update and boundary structure. Consult useTransition before marking updates nonurgent; typing into a controlled input must remain responsive and urgent.
Reserve plausible space for pending content, but avoid skeletons that suggest exact data you cannot predict. Keep status announcements short. Repeated nested live announcements can become more disruptive than a plain message. The accessible interface guide is useful when reviewing keyboard paths and announcements across these states.
Treat stale content as an explicit product choice. Keeping yesterday's balance visible while fetching today's value may preserve context, but users need a clear updating indicator and safeguards against acting on outdated information. A spinner alone does not explain whether the visible information is current.
Make recovery specific and testable
For query failures, retry semantics should include both clearing the boundary state and resetting or refetching the failed query through the data library. For a deterministic rendering bug, retrying the same inputs usually reproduces the crash. Offer an alternative route or support reference instead of encouraging endless clicks.
Test a slow response, a rejected request, a malformed response, and a component that deliberately throws. Verify each at both initial entry and client-side navigation. Also test when another region contains unsaved input. The central question is not merely whether the fallback appears, but whether unaffected work survives.
Add failure observability with boundary identity, route, release identifier, and a sanitized error category. Avoid logging entire response bodies or form values. Correlate repeated failures without exposing sensitive content in messages shown to users.
Finally, review the recovery copy with someone who did not implement the feature. They should understand what failed, what still works, and what the next action risks. Record these scenarios alongside your component examples so later refactors preserve the intended behavior rather than merely preserving the happy-path screenshot.
Key takeaways
- Use task-sized boundaries to isolate optional failures without hiding navigation or unrelated work.
- Suspense requires supported pending sources; ordinary effect-based fetching does not activate it automatically.
- Handle event and mutation failures explicitly instead of expecting render boundaries to catch everything.
- Keep recovery honest: query resets, asset reloads, and deterministic crashes need different responses.
- Test pending, failed, stale, and recovered states with keyboard navigation and unsaved work present.
Reach designers & developers at work
Put your product next to the tools people use every day. One sponsor per placement, no tracking pixels.
Become a sponsor