No app is perfect. Even if we try our best to test all possible cases, sometimes things will go wrong. It only takes one failing request to get you in trouble. It's how we handle it that makes the difference between an application crash and a graceful fail.
In this article, we'll cover the basics of error and exception handling in React apps. We're going to explore different kinds of errors and best practices to recover from them in a user-friendly way.
Errors can be caused by many different issues. It could be network issues, an external API that's currently down, unexpected data inputs or (as always) coding mistakes. Any of these could cause your app to crash, which we want to avoid at all costs. Getting to a white page with no way forward impacts your user's trust: Your app will feel unreliable and flaky to them.
Especially critical tasks like payments and user submissions should be handled with care. You don't want your users to sit in front of a crashed e-commerce app, asking themselves "Did my order go through anyway?".
We want our app to fail gracefully, meaning that our UI should still be responsive if an error occurs, and we'll do our best to recover quickly.
Error handling helps us to:
The most important concept to understand for React error handling are error boundaries – little safety nets for your app.
Error boundaries are React components that catch JS errors during rendering, in lifecycle methods and in constructors in the whole tree below them. You can then log those errors and display a fallback UI.
An error boundary component can define the lifecycle methods static getDerivedStateFromError(error)
to update the state and render()
a fallback UI or/and componentDidCatch(error, info)
to log error information.
Here's an example of an ErrorBoundary
class component:
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, info) {
console.error('Error caught by ErrorBoundary:', error, info);
// Log to an error monitoring service
errorService.log({ error, info });
}
render() {
const { hasError, error } = this.state;
if (hasError) {
// You can render any custom fallback UI
return (
<div>
<p>Oh no, something went wrong 🥺 - try again later!</p>
</div>
);
}
return this.props.children;
}
}
We can now wrap this component around our error-prone components to prevent an application crash. You can wrap small individual components or your entire app – the granularity is up to you.
<ErrorBoundary>
<Component />
</ErrorBoundary>
There are some limitations of error boundaries that we should know about. Error boundaries do not catch errors for:
So it won't work to just wrap event handlers or asynchronous code in an error boundary component, expecting it to capture errors. For operations like a fetch
call you will need to use try...catch
blocks as a catch-all way of handling errors.
async function fetchData() {
try {
const response = await fetch('/api/data');
const data = await response.json();
// Process data
} catch (error) {
// Handle the error
console.error(error);
}
}
However, it does work to combine error boundaries with Suspense, which lets you pause rendering while waiting for asynchronous processes to finish (like fetching data).
If an error occurs during loading, the error boundary can render a fallback UI at that position. For our UX that means we avoid a complete page refresh, and keep our app responsive and stable.
// AsyncComponentWithSuspense.js
const AsyncComponentWithSuspense = React.lazy(() => import('./SomeComponent'));
function MyComponent() {
return (
<ErrorBoundary>
<Suspense fallback={<div>Loading...</div>}>
<AsyncComponentWithSuspense />
</Suspense>
</ErrorBoundary>
);
}
While you can write your own ErrorBoundary components for full control, in many cases it will make sense to check out the react-error-boundary
package.
It provides a pre-built component for a simple setup and additional features for resetting error boundaries, support for hooks, an easy integration with error logging services and an easy way to specify a fallback component.
As an example on how the package makes handling errors easier, we'll take a look at the onReset
prop that we can use to reset a component when an error is thrown. This makes it super easy for us to build a retry mechanism for example:
function fallbackRender({ error, resetErrorBoundary }) {
return (
<div role="alert">
<p>Oh no, something went wrong 🥺: {error.message}</p>
<button onClick={resetErrorBoundary}>Try again</button>
</div>
);
}
<ErrorBoundary
fallbackRender={fallbackRender}
onReset={() => {
// Reset the state of your app
}}
>
<ExampleApplication />
</ErrorBoundary>;
When the resetErrorBoundary
function is called by clicking the button, the error boundary will be reset and the rendering will be repeated.
React 19 additionally provides onCaughtError
and onUncaughtError
hooks that are called when React catches an error in an Error Boundary (onCaughtError
) or when an error is thrown and not caught by an Error Boundary (unUncaughtError
).
We can add these hooks for centralized error processing (and reporting) at the root level and outside of the scope of Error Boundaries. Going forward, they will help us handle errors globally in a flexible, but also consistent way.
Here are some best practices for error handling at various levels of your app, so you can use error boundaries strategically and provide a smooth UX.
Now we know the basics of how to implement error handling, we'll talk about tools that are important for your workflow.
For simple logging while we are developing, we can use console logs:
console.log("Logging things while processes are running");
console.error("An error occured:" + myObject);
The console lets you view and inspect your logs, providing detailed error stack traces including line numbers & functions.
The React Developer Tools and Redux DevTools browser extensions further enhance your debugging experience.
Using these tools lets you see which components throw an error in the component tree, check component state (and state changes), monitor props and even track performance issues.
While logging is great during developement, we will need to persist our logs somewhere to know about errors that happen to our users.
Using an error monitoring service helps us track, log and analyze production issues. We can get notifications and detailed reports about any error, including infos about which users are impacted, breadcrumbs or even session replays. Having an error monitoring service in our workflow definitely brings us peace of mind, as we're running a SaaS product that a lot of people rely on daily.
There are many services available that integrate well with React. Personally, we have been using Sentry since forever (Disclaimer: They are also sponsors of @MadeWithReactJS), but they all more less work the same: They capture errors and uncaught exceptions in production once you added and configured their SDKs.
Sentry for example exports a custom ErrorBoundary
component that captures the original rendering error and attaches the component stack that the Error Boundary generates.
<Sentry.ErrorBoundary fallback={<p>An error has occurred</p>}>
<Example />
</Sentry.ErrorBoundary>;
For apps using React 19, you can also use the onCaughtError
/ onUncaughtError
hooks to capture errors automatically with Sentry.
import { createRoot } from "react-dom/client";
const container = document.getElementById(“app”);
const root = createRoot(container, {
// Error is thrown and not caught by ErrorBoundary
onUncaughtError: Sentry.reactErrorHandler((error, errorInfo) => {
console.warn('Uncaught error', error, errorInfo.componentStack);
}),
// Error caught in ErrorBoundary
onCaughtError: Sentry.reactErrorHandler(),
onRecoverableError: Sentry.reactErrorHandler(),
});
root.render();
Error handling is something that you have to customize for your app and its processes. While there is no one right way to do it, we covered the most important techniques, tools and workflows specifically for React in this article.
We hope you got a nice overview about how to build resilient apps #madewithreactjs!