This site runs best with JavaScript enabled.

Use react-error-boundary to handle errors in React


How to simplify your React apps by handling React errors effectively with react-error-boundary

What's wrong with this code?

1import * as React from 'react'
2import ReactDOM from 'react-dom'
3
4function Greeting({subject}) {
5 return <div>Hello {subject.toUpperCase()}</div>
6}
7
8function Farewell({subject}) {
9 return <div>Goodbye {subject.toUpperCase()}</div>
10}
11
12function App() {
13 return (
14 <div>
15 <Greeting />
16 <Farewell />
17 </div>
18 )
19}
20
21ReactDOM.render(<App />, document.getElementById('root'))

If you send that to production, your users are going to get the white screen of sadness:

Chrome window with nothing but white

If you run this with create-react-app's error overlay (during development), you'll get this:

TypeError Cannot read property 'toUpperCase' of undefined

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:

1import * as React from 'react'
2import ReactDOM from 'react-dom'
3
4function ErrorFallback({error}) {
5 return (
6 <div role="alert">
7 <p>Something went wrong:</p>
8 <pre style={{color: 'red'}}>{error.message}</pre>
9 </div>
10 )
11}
12
13function Greeting({subject}) {
14 try {
15 return <div>Hello {subject.toUpperCase()}</div>
16 } catch (error) {
17 return <ErrorFallback error={error} />
18 }
19}
20
21function Farewell({subject}) {
22 try {
23 return <div>Goodbye {subject.toUpperCase()}</div>
24 } catch (error) {
25 return <ErrorFallback error={error} />
26 }
27}
28
29function App() {
30 return (
31 <div>
32 <Greeting />
33 <Farewell />
34 </div>
35 )
36}
37
38ReactDOM.render(<App />, document.getElementById('root'))

That "works":

Something went wrong:

Cannot read property 'toUpperCase' of undefined

Something went wrong:

Cannot read property 'toUpperCase' of undefined

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:

1import * as React from 'react'
2import ReactDOM from 'react-dom'
3
4function ErrorFallback({error}) {
5 return (
6 <div role="alert">
7 <p>Something went wrong:</p>
8 <pre style={{color: 'red'}}>{error.message}</pre>
9 </div>
10 )
11}
12
13function Greeting({subject}) {
14 return <div>Hello {subject.toUpperCase()}</div>
15}
16
17function Farewell({subject}) {
18 return <div>Goodbye {subject.toUpperCase()}</div>
19}
20
21function App() {
22 try {
23 return (
24 <div>
25 <Greeting />
26 <Farewell />
27 </div>
28 )
29 } catch (error) {
30 return <ErrorFallback error={error} />
31 }
32}
33
34ReactDOM.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:

  1. It must be a class component 🙁
  2. It must implement either getDerivedStateFromError or componentDidCatch.

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:

1import * as React from 'react'
2import ReactDOM from 'react-dom'
3import {ErrorBoundary} from 'react-error-boundary'
4
5function ErrorFallback({error}) {
6 return (
7 <div role="alert">
8 <p>Something went wrong:</p>
9 <pre style={{color: 'red'}}>{error.message}</pre>
10 </div>
11 )
12}
13
14function Greeting({subject}) {
15 return <div>Hello {subject.toUpperCase()}</div>
16}
17
18function Farewell({subject}) {
19 return <div>Goodbye {subject.toUpperCase()}</div>
20}
21
22function App() {
23 return (
24 <div>
25 <ErrorBoundary FallbackComponent={ErrorFallback}>
26 <Greeting />
27 <Farewell />
28 </ErrorBoundary>
29 </div>
30 )
31}
32
33ReactDOM.render(<App />, document.getElementById('root'))

And that works perfectly:

Something went wrong:

TypeError: Cannot read property 'toUpperCase' of undefined

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:

1function ErrorFallback({error, resetErrorBoundary}) {
2 return (
3 <div role="alert">
4 <p>Something went wrong:</p>
5 <pre style={{color: 'red'}}>{error.message}</pre>
6 <button onClick={resetErrorBoundary}>Try again</button>
7 </div>
8 )
9}
10
11function Bomb({username}) {
12 if (username === 'bomb') {
13 throw new Error('💥 CABOOM 💥')
14 }
15 return `Hi ${username}`
16}
17
18function App() {
19 const [username, setUsername] = React.useState('')
20 const usernameRef = React.useRef(null)
21
22 return (
23 <div>
24 <label>
25 {`Username (don't type "bomb"): `}
26 <input
27 placeholder={`type "bomb"`}
28 value={username}
29 onChange={e => setUsername(e.target.value)}
30 ref={usernameRef}
31 />
32 </label>
33 <div>
34 <ErrorBoundary
35 FallbackComponent={ErrorFallback}
36 onReset={() => {
37 setUsername('')
38 usernameRef.current.focus()
39 }}
40 resetKeys={[username]}
41 >
42 <Bomb username={username} />
43 </ErrorBoundary>
44 </div>
45 </div>
46 )
47}

Here's what that experience is like:

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:

1function Greeting() {
2 const [{status, greeting, error}, setState] = React.useState({
3 status: 'idle',
4 greeting: null,
5 error: null,
6 })
7
8 function handleSubmit(event) {
9 event.preventDefault()
10 const name = event.target.elements.name.value
11 setState({status: 'pending'})
12 fetchGreeting(name).then(
13 newGreeting => setState({greeting: newGreeting, status: 'resolved'}),
14 newError => setState({error: newError, status: 'rejected'}),
15 )
16 }
17
18 return status === 'rejected' ? (
19 <ErrorFallback error={error} />
20 ) : status === 'resolved' ? (
21 <div>{greeting}</div>
22 ) : (
23 <form onSubmit={handleSubmit}>
24 <label>Name</label>
25 <input id="name" />
26 <button type="submit" onClick={handleClick}>
27 get a greeting
28 </button>
29 </form>
30 )
31}

Unfortunately, doing things that way means that you have to maintain TWO ways to handle errors:

  1. Runtime errors
  2. 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:

1function Greeting() {
2 const [{status, greeting}, setState] = React.useState({
3 status: 'idle',
4 greeting: null,
5 })
6 const handleError = useErrorHandler()
7
8 function handleSubmit(event) {
9 event.preventDefault()
10 const name = event.target.elements.name.value
11 setState({status: 'pending'})
12 fetchGreeting(name).then(
13 newGreeting => setState({greeting: newGreeting, status: 'resolved'}),
14 error => handleError(error),
15 )
16 }
17
18 return status === 'resolved' ? (
19 <div>{greeting}</div>
20 ) : (
21 <form onSubmit={handleSubmit}>
22 <label>Name</label>
23 <input id="name" />
24 <button type="submit" onClick={handleClick}>
25 get a greeting
26 </button>
27 </form>
28 )
29}

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:

1function Greeting() {
2 const [name, setName] = React.useState('')
3 const {status, greeting, error} = useGreeting(name)
4 useErrorHandler(error)
5
6 function handleSubmit(event) {
7 event.preventDefault()
8 const name = event.target.elements.name.value
9 setName(name)
10 }
11
12 return status === 'resolved' ? (
13 <div>{greeting}</div>
14 ) : (
15 <form onSubmit={handleSubmit}>
16 <label>Name</label>
17 <input id="name" />
18 <button type="submit" onClick={handleClick}>
19 get a greeting
20 </button>
21 </form>
22 )
23}

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:

1const ui = (
2 <ErrorBoundary FallbackComponent={ErrorFallback}>
3 <Greeting />
4 </ErrorBoundary>
5)

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.

Discuss on TwitterEdit post on GitHub

Share article
EpicReact.Dev

Get Really Good at React

Get yourself the most comprehensive guide to React for professional developers in the universe.

Blast Off

Write professional React.

Kent C. Dodds

Kent C. Dodds is a JavaScript software engineer and teacher. He's taught hundreds of thousands of people how to make the world a better place with quality software development tools and practices. He lives with his wife and four kids in Utah.

Join the Newsletter



Kent C. Dodds