
Stop Crashing the Whole App: Master React Error Boundary & Suspense
One typo in a component crashes the entire page? Learn how to use Error Boundaries to isolate crashes and show fallback UIs gracefully.

One typo in a component crashes the entire page? Learn how to use Error Boundaries to isolate crashes and show fallback UIs gracefully.
Yellow stripes appear when the keyboard pops up? Learn how to handle layout overflows using resizeToAvoidBottomInset, SingleChildScrollView, and tricks for chat apps.

Obsessively wrapping everything in `useMemo`? It might be hurting your performance. Learn the hidden costs of memoization and when to actually use it.

Deployed your React app and getting 404 on refresh? Here's why Client-Side Routing breaks on static servers and how to fix it using Nginx, AWS S3, Apache, and Netlify redirects. Includes a debugging guide.

Rebuilding a real house is expensive. Smart remodeling by checking blueprints (Virtual DOM) first.

I recently launched a shopping mall site. On opening day, I was excitedly watching the traffic logs when customer support messages started pouring in. "Every time I click 'Add to Cart', the screen goes white." "Refreshing the page doesn't help. It's just blank."
I investigated and found a trivial typo in the 'Add to Cart' function.
Uncaught Error: Cannot read properties of undefined (reading 'push')
The problem was, because of this one tiny error, the Header, Footer, Product List, and even the Navigation Bar—the entire UI—disappeared, leaving only a White Screen of Death. Users assumed the site was broken and left. It felt incredibly unfair that a single broken button could bring down the entire house.
I naively thought, "Why can't it just hide the broken part?"
Thinking back to jQuery or pure HTML days, if an image tag broke (<img> showing broken icon), the surrounding text remained visible. A JavaScript error might log a red line in the console, but other buttons usually kept working.
But React has a different philosophy based on its Single Tree Architecture.
If an error occurs anywhere in the tree during the Render Phase using JavaScript, React decides, "It is better to show nothing than to show a corrupted UI."
Leaving a UI in a corrupted state might lead users to interact with wrong data, potentially causing bigger issues (like incorrect payments).
So, React propagates the error from the failing component all the way up to the Root, effectively Unmounting the entire App. This is what causes the white screen.
I finally understood this by comparing it to a "Circuit Breaker" in a house.
We need a safety mechanism that, instead of a white screen, shows a Fallback UI: "Something went wrong. Please try again." This is exactly what an Error Boundary does.
react-error-boundaryThe official React docs show examples of implementing Error Boundaries using Class Components with componentDidCatch. But I didn't want to use Class Components in 2025.
So, I adopted the standard library react-error-boundary.
Wrap dangerous components—especially those fetching APIs or containing complex logic—with the boundary. I isolated major sections to prevent the whole page from dying.
import { ErrorBoundary } from 'react-error-boundary';
function App() {
return (
<Layout>
{/* 1. Even if Sidebar dies, Main Content survives */}
<ErrorBoundary FallbackComponent={SidebarFallback}>
<Sidebar />
</ErrorBoundary>
{/* 2. Even if Content dies, Header survives */}
<ErrorBoundary FallbackComponent={MainFallback}>
<ProductList />
</ErrorBoundary>
</Layout>
);
}
Now, even if ProductList crashes, Sidebar and Layout remain visible. Users can still navigate to other pages. This alone massively improves User Experience (UX).
Showing "Error occurred" is only helpful for developers. You need to tell users "What went wrong and What to do."
function ErrorFallback({ error, resetErrorBoundary }) {
return (
<div role="alert" className="error-container">
<h3>⚠️ Oops! Failed to load products.</h3>
<p>Could you try again in a moment?</p>
{/* Show details only in Dev */}
{process.env.NODE_ENV === 'development' && (
<pre>{error.message}</pre>
)}
{/* The most important part: Retry Button */}
<button onClick={resetErrorBoundary}>
Try Again
</button>
</div>
);
}
The resetErrorBoundary prop is key here. When called, the Error Boundary resets its error state and attempts to Re-mount the child component (ProductList). If the error was a temporary network glitch, this button alone can restore the app to normal.
If you are using React Query, simply re-rendering isn't enough. You must also clear the cached error data so fetch is triggered again.
react-error-boundary and tanstack-query work perfectly together.
import { QueryErrorResetBoundary } from '@tanstack/react-query';
import { ErrorBoundary } from 'react-error-boundary';
<QueryErrorResetBoundary>
{({ reset }) => (
<ErrorBoundary
onReset={reset} // Resets the query error here!
FallbackComponent={ErrorFallback}
>
<ProductList />
</ErrorBoundary>
)}
</QueryErrorResetBoundary>
Now, clicking "Try Again" does three things: ① Clears query error cache, ② Re-mounts the component, ③ Triggers a fresh API request. A perfect retry mechanism.
After applying Error Boundary, I ran some tests and found something strange.
Errors occurring inside useEffect fetch calls were NOT caught by the Error Boundary, resulting in a white screen.
// ❌ Error Boundary fails to catch this
useEffect(() => {
fetch('/api/data').then(res => {
if (!res.ok) throw new Error('API Fail'); // Error here!
});
}, []);
React Error Boundaries only catch errors during the Render Phase, Lifecycle Methods, and Constructors.
They do NOT catch errors in: Async callbacks (setTimeout, Promise), Event handlers (onClick), or Server-Side Rendering (SSR). These happen outside React's render flow.
useErrorBoundary Hook (Explicit Throw)You can use the hook provided by the library to throw errors into the render cycle effectively.
import { useErrorBoundary } from 'react-error-boundary';
function MyComponent() {
const { showBoundary } = useErrorBoundary();
useEffect(() => {
fetch('/api/data').catch((error) => {
// Delegate error to Error Boundary!
showBoundary(error);
});
}, []);
// ...
}
The most elegant way is to use React Query's suspense: true option (or useSuspenseQuery in v5).
With this, Suspense handles the loading state, and data fetching errors are propagated as Render Errors, which ErrorBoundary catches naturally.
{/* 3. Handle Errors */}
<ErrorBoundary fallback={<div>Something went wrong</div>}>
{/* 2. Handle Loading */}
<Suspense fallback={<Skeleton />}>
{/* 1. Render Data (Success Case) */}
<AsyncProductList />
</Suspense>
</ErrorBoundary>
Look at this code. No messy if (isLoading) return ... or if (isError) return ... checks.
You only write the Success UI. Parents (Suspense, ErrorBoundary) take responsibility for loading and errors. This is the essence of Declarative Programming.
Error Boundaries are not just for showing UI; they are the perfect place to log errors. Developers should know about the crash before the user reports it.
Use the onError prop to execute logic when an error is caught.
<ErrorBoundary
FallbackComponent={ErrorFallback}
onError={(error, info) => {
console.error("Error Caught:", error);
// Send to Sentry, LogRocket, etc.
Sentry.captureException(error, {
extra: {
componentStack: info.componentStack,
userAction: "Clicked Add to Cart", // Add context
}
});
}}
>
<App />
</ErrorBoundary>
This ensures you get precise logs: "Which component crashed" and "What was the stack trace." Connect this to Slack alerts, and you can fix bugs while others sleep.
ErrorBoundary to isolate failures by section, and combine it with Suspense for declarative handling. Giving users a 'Try Again' button instead of a blank screen is the last line of defense for a developer's conscience.