You may have heard that handling error is an important programming skill. But why? What will happen if you don’t handle errors in React?
I am sure you must have wondered it.
It depends. But most of the time react will not show anything. It will show that white screen of death with no way of getting back to the application.
Visit this project to check for yourself
By handling errors yourself, you can:
- provide a useful message for the user; and also
- allow users to recover from the error.
Now you know how important error handling is, where do we go from here?
There are 2 ways to handle errors in react. Let’s see both of them one by one.
Method 1: Using try-catch
try-catch
is one way of handling errors. It is straightforward to implement.
function ErrorFallback({error}) {
return (
<div role="alert">
<p>Something went wrong:</p>
<pre>{error.message}</pre>
</div>
)
}
const YourName = ({ name }) => {
try {
return <p>HELLO {name.toUpperCase()}</p>
} catch(e) {
return <ErrorFallback error={e} />
}
};
export default function ui() {
const [name, setName] = React.useState('');
const handleInputChange = (e) => {
setName(e.target.value);
};
return (
<>
<input
type="text"
id="pokemon"
value={name}
onChange={handleInputChange}
placeholder='Your Name'
/>
<button onClick={() => setName(undefined)}>Throw an error!</button>
<YourName name={name} />
</>
)
}
In this example I’ve created an input which changes name
prop of YourName
component. When you click the button, it will set name to undefined
and throw an error.
But try-catch
is not recommended for error handling in react. Why?
try-catch
statements are imperative
Imperative means that try-catch
explicitly spell out each step of how you want something done. Whereas in declarative code, you merely say what it is that you want done.
If you compare try-catch
with Error Boundaries
you will know what is imperative and what is declarative.
try-catch
statements need to explicitly check errors for each component
You can’t add try-catch
block at the top level of your application.
export default function ErrorHandlingTry() {
const [name, setName] = React.useState('');
const handleInputChange = (e) => {
setName(e.target.value);
};
try {
return (
<>
<input
type="text"
id="pokemon"
value={name}
onChange={handleInputChange}
placeholder='Your Name'
/>
<button className='outline secondary' onClick={() => setName(undefined)}>Click me to throw an error!</button>
<YourName name={name} />
</>
)
} catch(e) {
return <ErrorFallback error={e} />
}
}
This is because we are not calling components ourselves. React is calling this component when rendering the application UI.
So if you want to use try-catch
, you need to check for each individual component that might throw an error.
Method #2: React Error Boundary
Error boundaries were a new feature introduced in React 16. React docs defined error boundaries as:
React components that catch JavaScript errors anywhere in their child component tree, log those errors, and display a fallback UI instead of the component tree that crashed.
And one of the best things 💕 ?
You can declare an error boundary component once and use it throughout your application.
Here’s how you build an error boundary comopnent:
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
componentDidCatch(error) {
this.setState({ hasError: true });
}
render() {
const {hasError} = this.state
if (hasError) {
return <this.props.FallbackComponent error={hasError} />
}
return this.props.children
}
}
Here are a few things to remember about the error boundary component:
- It must be a class component.
- It must implement either
getDerivedStateFromError
orcomponentDidCatch
.
I know building a class component isn’t super inspiring 😢. This is why I use the [react-error-boundary](https://github.com/bvaughn/react-error-boundary)
component.
Step 1: Getting Started with React Error Boundary
Here’s how to use it:
import {ErrorBoundary} from 'react-error-boundary'
export default function ui() {
const [name, setName] = React.useState('');
const handleInputChange = (e) => {
setName(e.target.value);
};
return (
<ErrorBoundary FallbackComponent={ErrorFallback}>
<input
type="text"
id="pokemon"
value={name}
onChange={handleInputChange}
placeholder='Your Name'
/>
<button onClick={() => setName(undefined)}>Throw an error!</button>
<YourName name={name} />
</ErrorBoundary>
)
}
Step 2: How to recover from error?
One of the best feature of react-error-boundary
is error recovery. You can pass onReset
and resetKeys
props to recover from the error.
const ui = (
<ErrorBoundary
FallbackComponent={ErrorFallback}
onReset={() => setName('')}
resetKeys={[name]}
>
{children}
</ErrorBoundary>
)
And then in your fallback component you can accept resetErrorBoundary
prop and pass it any element.
function ErrorFallback({ error, resetErrorBoundary }) {
return (
<div role="alert">
<p>Something went wrong:</p>
<pre>{error.message}</pre>
<button onClick={resetErrorBoundary}>Reset Error</button>
</div>
);
}
Errors that can’t be handled by Error Boundary
Everything isn’t hunky-dory with error boundaries. They can’t catch some errors in your component. To quote react docs:
Error boundaries do not catch errors for:
- Event handlers (learn more)
- Asynchronous code (e.g.
setTimeout
orrequestAnimationFrame
callbacks)- Server-side rendering
- Errors are thrown in the error boundary itself (rather than its children)
For catching errors thrown in event handlers or asynchronous code, I use useErrorHandler
hook provided by react-error-boundary
.
You can pass errors to this hook to show your fallback component.