Suspense & Error Boundaries
Building robust React applications involves handling two inevitable scenarios: waiting for data/code to load, and dealing with unexpected errors. React provides two powerful features to manage these gracefully: Suspense and Error Boundaries.
React Suspense
Suspense lets your components "wait" for something before they render. Today, it is primarily used for two things:
- Code splitting: Waiting for components to load lazily via
React.lazy. - Data fetching: Waiting for data to be fetched (often utilized with frameworks like Next.js, Remix, or libraries like Relay and React Query).
Basic Usage with React.lazy
import { Suspense, lazy } from 'react';
// Lazily load the component
const HeavyChart = lazy(() => import('./HeavyChart'));
function Dashboard() {
return (
<div>
<h1>Dashboard</h1>
{/* Provide a fallback UI while HeavyChart is loading */}
<Suspense fallback={<Spinner />}>
<HeavyChart />
</Suspense>
</div>
);
}
Suspense for Data Fetching
In modern React (especially with Server Components or concurrent rendering), Suspense can boundary data fetching. When a component reads data that isn't ready yet, it "suspends" rendering and React shows the nearest Suspense fallback.
<Suspense fallback={<SkeletonProfile />}>
<UserProfile userId={123} />
</Suspense>
Error Boundaries
An Error Boundary is a React component that catches JavaScript errors anywhere in its child component tree, logs those errors, and displays a fallback UI instead of crashing the whole application.
Currently, Error Boundaries must be written as Class Components.
Creating an Error Boundary
import React from 'react';
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
// Update state so the next render shows the fallback UI.
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
// You can also log the error to an error reporting service like Sentry
console.error("Uncaught error:", error, errorInfo);
}
render() {
if (this.state.hasError) {
// You can render any custom fallback UI
return <h2>Something went wrong.</h2>;
}
return this.props.children;
}
}
Combining Suspense and Error Boundaries
In real-world applications, you often wrap your asynchronous components in both an Error Boundary and a Suspense boundary. This handles both the "loading" state and the "failed" state.
<ErrorBoundary fallback={<ErrorMessage />}>
<Suspense fallback={<LoadingSpinner />}>
<Feed />
</Suspense>
</ErrorBoundary>
If Feed is fetching data:
- While it fetches, it suspends. React renders
<LoadingSpinner />. - If the fetch fails and throws an error, the
ErrorBoundarycatches it. React renders<ErrorMessage />.
Best Practices
- Granular Boundaries: Don't just put one giant Suspense/Error boundary at the root of your app. Wrap specific sections (like a sidebar, a main content area) so that an error or delay in one part doesn't take down the entire page.
- Use Libraries: Libraries like
react-error-boundaryprovide functional components and hooks (useErrorHandler) that make working with error boundaries much easier without writing class components yourself.