While catching errors before they hit production is ideal, some of them, such as network errors, might slip through testing and impact your users.
If your React components are not properly catching errors thrown by third-party libraries or React hooks, such errors either end-up crashing the React lifecycle or reaching the top-level of the main execution thread, resulting in the “white screen” scenario:
As of React 16, errors that were not caught […] will result in unmounting of the whole React component tree
It is crucial that your application gracefully handle such errors by providing proper visual feedback and potential actions (ex: retry mechanisms).
Fortunately, implementing such UX patterns can be achieved with little work with the React API and, for the most advanced UX, with the help of lightweight React libraries.
Using JavaScript’s try-catch
around React hooks calls won’t work due to the asynchronous nature of their execution. However, React API offers the Error boundaries mechanism to catch all types of errors that might “bubble out” from a component.
For example, if the <ComponentA />
is wrapped in a React Error boundary, the error propagation will stop at the Error Boundary level, preventing the React App from crashing:
This article will cover how to implement Error Boundaries in your application, from simple error catching to displaying visual feedback and providing retry mechanisms.
Simple Error Boundaries: Catching and Reporting Errors
Behind its sophisticated name, an Error Boundary is just a plain class React component implementing the componentDidCatch(error)
method:
class ErrorBoundarySimple extends React.Component {
componentDidCatch(error) {
// report the error to your favorite Error Tracking tool (ex: Sentry, Bugsnag)
}
render() {
return this.props.children;
}
}
Note: React is not yet offering a hook-based alternative to implement error boundaries.
As showcased in this CodeSandbox, the componentDidCatch()
class method will be called as soon as an error reaches our MyErrorBoundary
component, allowing us to prevent the React app from crashing and forwarding the error to our error reporting tool. (The CodeSandbox might display a development error overlay that only shows in development, you can dismiss it to see the rendering result).
Let’s make our <ErrorBoundarySimple>
more friendly by adding simple visual feedback when errors are raised. For this, we add some state to ErrorBoundarySimple
and use the getDerivedStateFromError()
method, as follows:
class ErrorBoundarySimple extends React.Component {
state = { hasError: false };
componentDidCatch(error: unknown) {
// report the error to your favorite Error Tracking tool (ex: Sentry, Bugsnag)
console.error(error);
}
static getDerivedStateFromError(error: unknown) {
// Update state so the next render will show the fallback UI.
return { hasError: true };
}
render() {
if (this.state.hasError) {
return <p>Failed to fetch users.</p>;
}
return this.props.children;
}
}
React expects the detDerivedStateFromError()
method to return the state value to apply to out <ErrorBoundarySimple>
when an error occurs.
As we can see on our live CodeSandbox, our UI is now providing visual feedback!
Error boundaries can also be nested to provide more contextualized feedback. For example, in this React app tree, we might want to provide different feedback based on what is crashing. For instance, we may want to provide different feedback when the Chat is crashing and when the TodoList is crashing, yet still handle any kind of crash at the application level. We can introduce multiple Boundaries to achieve this:
With the above setup, any error in the <Chat>
component (or its descendant) would be caught in the Error Boundary wrapping the <Chat>
component (not the “App” Error Boundary), allowing us to give a contextualized visual feedback. However, any error coming from all <App>
descendants (excluding <Chat>
and <TodoList>
) will be caught by the “App” Error Boundary.
With a few lines of code, we just greatly improved our user experience by gracefully handling errors in our application.
However, such simple Error Boundaries implementations do have limitations. First, according to the React documentation, Error boundaries do not catch errors for:
- Event handlers
- Asynchronous code (e.g. setTimeout or requestAnimationFrame callbacks)
- Server-side rendering
- Errors thrown in the error boundary itself (rather than its children)
And, the previously showcased Error Boundaries do not provide any action to the user to recover from the error, for example, with a retry mechanism. In the next section, we will see how to leverage the react-error-boundary
library to handle all these edge cases.
Advanced Error Boundaries: Catching all Errors and Retry Mechanisms
Let’s now provide a superior error handling user experience by catching all kinds of errors and exposing recovery actions to the users. For this, we will use the react-error-boundary
library which can be installed as follows:
npm install --save react-error-boundary
yarn add react-error-boundary
Provide a Retry Mechanism
Our new CodeSandbox defines a <Users>
component that will fail to load users 50% of the time. (The CodeSandbox might display a development error overlay that only shows in development, you can dismiss it to see the rendering result).
Let’s use react-error-boundary
to properly catch errors and provide a retry mechanism:
import { ErrorBoundary, FallbackProps } from "react-error-boundary";
import { Users } from "./Users";
function ErrorFallback({ error, resetErrorBoundary }: FallbackProps) {
return (
<div role="alert">
<p>Failed to load users:</p>
<pre>{error.message}</pre>
<button onClick={resetErrorBoundary}>Try again</button>
</div>
);
}
export default function App(): JSX.Element {
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
<ErrorBoundary FallbackComponent={ErrorFallback}>
{/* Users will fail to load 50% of the time */}
<Users />
</ErrorBoundary>
</div>
);
}
<ErrorBoundary>
takes one mandatory FallbackComponent=
prop that should be the react component or JSX that will be rendered in case of error. In the case of a component, this FallbackComponent=
function will receive FallbackProps
:
error
can be used to display the error.resetErrorBoundary
is a callback to reset the error state and re-render the children’s components.
An ononError
prop can also be provided to forward the error to your favorite error reporting tool (ex: Sentry). The react-error-boundary
documentation showcases how to leverage other props (ex: onReset=
) to handle more advanced scenarios.
Catching all Errors
As aforementioned, Error boundaries do not catch errors for:
- Event handlers (learn more)
- Asynchronous code (e.g. setTimeout or requestAnimationFrame callbacks)
Because such errors happen outside of the React rendering lifecycle, Error boundaries won’t be invoked. Again, react-error-boundary
has us covered by providing a handleError()
hook that helps with catching event-related and asynchronous errors.
import { useErrorHandler } from 'react-error-boundary'
function Greeting() {
const [greeting, setGreeting] = React.useState(null)
const handleError = useErrorHandler()
function handleSubmit(event) {
event.preventDefault()
const name = event.target.elements.name.value
fetchGreeting(name).then(
newGreeting => setGreeting(newGreeting),
error => handleError(error),
)
}
return greeting ? (
<div>{greeting}</div>
) : (
<form onSubmit={handleSubmit}>
<label>Name</label>
<input id="name" />
<button type="submit">get a greeting</button>
</form>
)
}
Errors happening inside of handleSubmit()
function won’t be caught by React rendering lifecycle. For this reason, we use the handleError
function provided by react-error-boundary
’s useErrorHandler()
to rethrow the error in the React lifecycle so that the nearest ErrorBoundary can catch it.
Conclusion
Behind its sophisticated name, a React Error Boundary is a straightforward way to gracefully handle any kind of error in a React application.
Good products should prevent errors from reaching production but also should use error boundaries to provide contextual feedback and recovery actions to their users in case of unexpected errors.
Meticulous
Meticulous is a tool for software engineers to catch visual regressions in web applications without writing or maintaining UI tests.
Inject the Meticulous snippet onto production or staging and dev environments. This snippet records user sessions by collecting clickstream and network data. When you post a pull request, Meticulous selects a subset of recorded sessions which are relevant and simulates these against the frontend of your application. Meticulous takes screenshots at key points and detects any visual differences. It posts those diffs in a comment for you to inspect in a few seconds. Meticulous automatically updates the baseline images after you merge your PR. This eliminates the setup and maintenance burden of UI testing.
Meticulous isolates the frontend code by mocking out all network calls, using the previously recorded network responses. This means Meticulous never causes side effects and you don’t need a staging environment.
Learn more here.
Authored by Charly Poly
use-error-boundary
A react hook for using error boundaries in your functional components.
It lets you keep track of the error state of child components, by wrapping them in the provided ErrorBoundary
component.
⚠️ Read more about error boundaries and their intended use in the React documentation, this will only catch errors during the render phase!
Installation
yarn add use-error-boundary
Breaking changes in 2.x
While upgrading from version 1.x
make sure you are not using the errorInfo
object.
The hook and the renderError
callback no longer provide this object.
For advanced use, please refer to Custom handling of error and errorInfo.
Examples and usage
Import the hook:
// Named import { useErrorBoundary } from "use-error-boundary" // Default import useErrorBoundary from "use-error-boundary"
Learn more about the properties that are returned.
const MyComponent = () => { const { ErrorBoundary, didCatch, error, reset } = useErrorBoundary() //... }
Usage without render props
Wrap your components in the provided ErrorBoundary
.
When it catches an error the hook provides you the changed error-state and the boundary Component will render nothing.
You have to handle rendering some error display yourself.
You can get the ErrorBoundary component to render your custom error display by using the renderError
render-prop.
const JustRenderMe = () => { throw new Error("💥") } const MyComponent = () => { const { ErrorBoundary, didCatch, error } = useErrorBoundary() return ( <> {didCatch ? ( <p>An error has been caught: {error.message}</p> ) : ( <ErrorBoundary> <JustRenderMe /> </ErrorBoundary> )} </> ) }
Usage with render props
Optionally, you can pass a render
and renderError
function to render your UI and error-state inside the boundary.
/** * The renderError function also passes the error, so that you can display it using * render props. */ return ( <ErrorBoundary render={() => <SomeChild />} renderError={({ error }) => <MyErrorComponent error={error} />} /> )
Handling error
and errorInfo
outside of markup
The hook now accepts an options
object that you can pass a onDidCatch
callback that gets called when the ErrorBoundary catches an error. Use this for logging or reporting of errors.
useErrorBoundary({ onDidCatch: (error, errorInfo) => { // For logging/reporting }, })
Returned Properties
These are the properties of the returned Object:
ErrorBoundary
Type: React Component
Special error boundary component that provides state changes to the hook.
⚠️ You need to use this as the error boundary! Otherwise, the state will not update when errors are caught!
The ErrorBoundary is guaranteed referential equality across rerenders and only updates after a reset.
didCatch
Type: boolean
The error state, true
if an error has ben caught.
error
Type: any | null
The error caught by the boundary, or null
.
reset
Type: function
Function the reset the error state.
Forces react to recreate the boundary by creating a new ErrorBoundary
Your boundary can now catch errors again.
If you are searching for the errorInfo
property, please read Breaking Changes in 2.x.
Why should I use this hook?
React does not provide a way to catch errors within the same functional component and you have to handle that in a class Component with special lifecycle methods.
If you are new to ErrorBoundaries, building this yourself is a good way to get started!
This packages purpose is to provide an easy drop in replacement for projects that are being migrated to hooks.
This also pulls the error presentation out of the error boundary, and on the same level you are handling errors.
Contributing
Contributions are always welcome.
Feel free to open issues or pull requests!
What’s wrong with this code?
import * as React from 'react'
import ReactDOM from 'react-dom'
function Greeting({subject}) {
return <div>Hello {subject.toUpperCase()}</div>
}
function Farewell({subject}) {
return <div>Goodbye {subject.toUpperCase()}</div>
}
function App() {
return (
<div>
<Greeting />
<Farewell />
</div>
)
}
ReactDOM.render(<App />, document.getElementById('root'))
If you send that to production, your users are going to get the white screen of
sadness:
If you run this with create-react-app’s error overlay (during development),
you’ll get this:
The problem is we need to either pass a subject
prop (as a string) or default
the subject
prop’s value. Obviously, this is contrived, but runtime errors
happen all of the time and that’s why it’s a good idea to gracefully handle such
errors. So let’s leave this error in for a moment and see what tools React has
for us to handle runtime errors like this.
try/catch?
The naive approach to handling this kind of error would be to add a try/catch
:
import * as React from 'react'
import ReactDOM from 'react-dom'
function ErrorFallback({error}) {
return (
<div role="alert">
<p>Something went wrong:</p>
<pre style={{color: 'red'}}>{error.message}</pre>
</div>
)
}
function Greeting({subject}) {
try {
return <div>Hello {subject.toUpperCase()}</div>
} catch (error) {
return <ErrorFallback error={error} />
}
}
function Farewell({subject}) {
try {
return <div>Goodbye {subject.toUpperCase()}</div>
} catch (error) {
return <ErrorFallback error={error} />
}
}
function App() {
return (
<div>
<Greeting />
<Farewell />
</div>
)
}
ReactDOM.render(<App />, document.getElementById('root'))
That «works»:
But, it may be ridiculous of me, but what if I don’t want to wrap every
component in my app in a try/catch
block? In regular JavaScript, you can
simply wrap the calling function in a try/catch
and it’ll catch any errors in
the functions it calls. Let’s try that here:
import * as React from 'react'
import ReactDOM from 'react-dom'
function ErrorFallback({error}) {
return (
<div role="alert">
<p>Something went wrong:</p>
<pre style={{color: 'red'}}>{error.message}</pre>
</div>
)
}
function Greeting({subject}) {
return <div>Hello {subject.toUpperCase()}</div>
}
function Farewell({subject}) {
return <div>Goodbye {subject.toUpperCase()}</div>
}
function App() {
try {
return (
<div>
<Greeting />
<Farewell />
</div>
)
} catch (error) {
return <ErrorFallback error={error} />
}
}
ReactDOM.render(<App />, document.getElementById('root'))
Unfortunately, this doesn’t work. And that’s because we’re not the ones calling
Greeting
and Farewell
. React calls those functions. When we use them in JSX,
we’re simply creating React elements with those functions as the type
. Telling
React that «if the App
is rendered, here are the other components that will
need to be called.» But we’re not actually calling them, so the try/catch
won’t work.
I’m not too disappointed by this to be honest, because try/catch
is inherently
imperative and I’d prefer a declarative way to handle errors in my app anyway.
React Error Boundary
This is where the
Error Boundary feature comes
in to play. An «Error Boundary» is a special component that you write to handle
runtime errors like those above. For a component to be an Error Boundary:
- It must be a class component 🙁
- It must implement either
getDerivedStateFromError
orcomponentDidCatch
.
Luckily, we have react-error-boundary
which exposes the last Error Boundary component anyone needs to write because it
gives you all the tools you need to declaratively handle runtime errors in your
React apps.
So let’s add react-error-boundary
and
render the ErrorBoundary
component:
import * as React from 'react'
import ReactDOM from 'react-dom'
import {ErrorBoundary} from 'react-error-boundary'
function ErrorFallback({error}) {
return (
<div role="alert">
<p>Something went wrong:</p>
<pre style={{color: 'red'}}>{error.message}</pre>
</div>
)
}
function Greeting({subject}) {
return <div>Hello {subject.toUpperCase()}</div>
}
function Farewell({subject}) {
return <div>Goodbye {subject.toUpperCase()}</div>
}
function App() {
return (
<div>
<ErrorBoundary FallbackComponent={ErrorFallback}>
<Greeting />
<Farewell />
</ErrorBoundary>
</div>
)
}
ReactDOM.render(<App />, document.getElementById('root'))
And that works perfectly:
Error Recovery
The nice thing about this is you can almost think about the ErrorBoundary
component the same way you do a try/catch
block. You can wrap it around a
bunch of React components to handle lots of errors, or you can scope it down to
a specific part of the tree to have more granular error handling and recovery.
react-error-boundary
gives us all the
tools we need to manage this as well.
Here’s a more complex example:
function ErrorFallback({error, resetErrorBoundary}) {
return (
<div role="alert">
<p>Something went wrong:</p>
<pre style={{color: 'red'}}>{error.message}</pre>
<button onClick={resetErrorBoundary}>Try again</button>
</div>
)
}
function Bomb({username}) {
if (username === 'bomb') {
throw new Error('💥 CABOOM 💥')
}
return `Hi ${username}`
}
function App() {
const [username, setUsername] = React.useState('')
const usernameRef = React.useRef(null)
return (
<div>
<label>
{`Username (don't type "bomb"): `}
<input
placeholder={`type "bomb"`}
value={username}
onChange={e => setUsername(e.target.value)}
ref={usernameRef}
/>
</label>
<div>
<ErrorBoundary
FallbackComponent={ErrorFallback}
onReset={() => {
setUsername('')
usernameRef.current.focus()
}}
resetKeys={[username]}
>
<Bomb username={username} />
</ErrorBoundary>
</div>
</div>
)
}
Here’s what that experience is like:
Username (don’t type «bomb»):
Hi
You’ll notice that if you type «bomb», the Bomb
component is replaced by the
ErrorFallback
component and you can recover by either changing the username
(because that’s in the resetKeys
prop) or by clicking «Try again» because
that’s wired up to resetErrorBoundary
and we have an onReset
that resets our
state to a username that won’t trigger the error all over again.
Handle all errors
Unfortunately, there are some errors that React doesn’t/can’t hand off to our
Error Boundary. To quote
the React docs:
Error boundaries do not catch errors for:
- Event handlers
(learn more)- Asynchronous code (e.g. setTimeout or requestAnimationFrame callbacks)
- Server side rendering
- Errors thrown in the error boundary itself (rather than its children)
Most of the time, folks will manage some error
state and render something
different in the event of an error, like so:
function Greeting() {
const [{status, greeting, error}, setState] = React.useState({
status: 'idle',
greeting: null,
error: null,
})
function handleSubmit(event) {
event.preventDefault()
const name = event.target.elements.name.value
setState({status: 'pending'})
fetchGreeting(name).then(
newGreeting => setState({greeting: newGreeting, status: 'resolved'}),
newError => setState({error: newError, status: 'rejected'}),
)
}
return status === 'rejected' ? (
<ErrorFallback error={error} />
) : status === 'resolved' ? (
<div>{greeting}</div>
) : (
<form onSubmit={handleSubmit}>
<label>Name</label>
<input id="name" />
<button type="submit" onClick={handleClick}>
get a greeting
</button>
</form>
)
}
Unfortunately, doing things that way means that you have to maintain TWO ways to
handle errors:
- Runtime errors
fetchGreeting
errors
Luckily, react-error-boundary
also
exposes a simple hook to help with these situations as well. Here’s how you
could use that to side-step this entirely:
function Greeting() {
const [{status, greeting}, setState] = React.useState({
status: 'idle',
greeting: null,
})
const handleError = useErrorHandler()
function handleSubmit(event) {
event.preventDefault()
const name = event.target.elements.name.value
setState({status: 'pending'})
fetchGreeting(name).then(
newGreeting => setState({greeting: newGreeting, status: 'resolved'}),
error => handleError(error),
)
}
return status === 'resolved' ? (
<div>{greeting}</div>
) : (
<form onSubmit={handleSubmit}>
<label>Name</label>
<input id="name" />
<button type="submit" onClick={handleClick}>
get a greeting
</button>
</form>
)
}
So when our fetchGreeting
promise is rejected, the handleError
function is
called with the error and react-error-boundary will make that propagate to the
nearest error boundary like usual.
Alternatively, let’s say you’re using a hook that gives you the error:
function Greeting() {
const [name, setName] = React.useState('')
const {status, greeting, error} = useGreeting(name)
useErrorHandler(error)
function handleSubmit(event) {
event.preventDefault()
const name = event.target.elements.name.value
setName(name)
}
return status === 'resolved' ? (
<div>{greeting}</div>
) : (
<form onSubmit={handleSubmit}>
<label>Name</label>
<input id="name" />
<button type="submit" onClick={handleClick}>
get a greeting
</button>
</form>
)
}
In this case, if the error
is ever set to a truthy value, then it will be
propagated to the nearest error boundary.
In either case, you could handle those errors like this:
const ui = (
<ErrorBoundary FallbackComponent={ErrorFallback}>
<Greeting />
</ErrorBoundary>
)
And now that’ll handle your runtime errors as well as the async errors in the
fetchGreeting
or useGreeting
code.
Note: you might be interested to know that the
implementation
of useErrorHandler
is only 6 lines long 😉
Conclusion
Error Boundaries have been a feature in React for years and we’re still in this
awkward situation of handling runtime errors with Error Boundaries and handling
other error states within our components when we would be much better off
reusing our Error Boundary components for both. If you haven’t already given
react-error-boundary
a try, definitely
give it a solid look!
Good luck.
Oh, one other thing. Right now, you may notice that you’ll experience that error
overlay even if the error was handled by your Error Boundary. This will only
happen during development (if you’re using a dev server that supports it, like
react-scripts, gatsby, or codesandbox). It won’t show up in production. Yes, I
agree this is annoying.
PRs welcome.