If we want to prevent our UI from crashing on errors and also have a fallback UI to show this errors in a friendly manner, we can use React error boundary components that wraps around critical parts of our app and catches JavaScript errors anywhere in it's child component tree.
Complete code example with typescript here.
Creating a custom error boundary component
Error boundaries are created as class components with access to two special lifecycle methods:
-
static getDerivedStateFromError()
which updates it's state to show the fallback UI. -
componentDidCatch()
used to log error information.
class ErrorBoundary extends React.Component {
state: State = {error: null}
static getDerivedStateFromError(error) {
return {error}
}
componentDidCatch(error, errorInfo) {
logErrorToMyService(error, errorInfo);
}
render() {
const {error} = this.state
if (error) {
return <this.props.FallbackComponent error={error} />
}
return this.props.children
}
}
In this example we are passing a FallbackComponent
to be rendered if our ErrorBoundary catches an error and we are logging the error to a external service.
To use the ErrorBoundary component in our application we just need to wrap it around a component that might come across some errors. In this example I wrapped a component that fetches data from an API and passed a fallback component that shows an error message if something goes wrong:
<ErrorBoundary
// use key as a workaround for resetting the errorboundary state
key={circuitName}
FallbackComponent={CircuitErrorFallback}
>
<CircuitContent />
</ErrorBoundary>
function CircuitErrorFallback({error}) {
return (
<div role="alert">
<h3>Something went wrong...</h3>
<p>{error.message}</p>
</div>
)
}
The <CircuitContent />
component will throw an error if something goes wrong with our API call:
function CircuitContent({circuitName}) {
const [state, setState] = useState<>({
status: 'idle',
circuit: {},
error: null,
})
const {status, circuit, error} = state
useEffect(() => {
if (!circuitName) {
return
}
setState(prevState => ({...prevState, status: 'pending'}))
fetchCircuit(circuitName).then(
circuit => {
setState(prevState => ({...prevState, status: 'resolved', circuit}))
},
error => {
setState(prevState => ({...prevState, status: 'rejected', error}))
},
)
}, [circuitName])
if (status === 'idle') {
return <CircuitIdle />
} else if (status === 'pending') {
return <CircuitLoading />
} else if (status === 'rejected') {
// throw error to be handled by error boundary
throw error
} else if (status === 'resolved') {
return <CircuitDetails circuit={circuit} />
}
throw new Error('Something went really wrong.')
}
And ErrorBoundary will catch this error and render our fallback component:
Using react-error-boundary
Creating our own error boundary component is pretty straight forward but we can also install react-error-boundary
package on our app and use it's features for resetting our error boundary and restoring the state of our UI.
import {ErrorBoundary} from 'react-error-boundary'
<ErrorBoundary
onReset={handleReset}
resetKeys={[circuitName]}
FallbackComponent={CircuitErrorFallback}
>
<CircuitContent circuitName={circuitName} />
</ErrorBoundary>
Now we can extend our fallback component with a button for reset the errorboundary:
function CircuitErrorFallback({ error, resetErrorBoundary }) {
return (
<div role="alert">
<h3>Something went wrong...</h3>
<p>{error.message}</p>
<button onClick={resetErrorBoundary}>
Try again
</button>
</div>
)
}
And the resulting error UI will look like this:
Conclusion
We can wrap different parts of our applications with error boundaries to keep our interface interactive and prevent crashing. This can also benefit us during development stage while catching errors that could even get unnoticed by typescript.
Note on usage with Create React App:
CRA may display an overlay with error information in development mode even if error boundary catches the error. There are workarounds to change this behavior of Create React App but I think it's unecessary, since you can press 'esc' to close the overlay and this will not be shown in production build anyway.
Tip for handling error messages with Axios:
Axios will throw an error with a custom message like "The server responded with 404 status code." when an API call fails. You can use an axios interceptor to change this custom message to the actual error message in the API response body or even map it to something else:
const api = axios.create({baseURL: 'https://api.backend.com'})
api.interceptors.response.use(
response => response,
error => {
if (error.response.data.message) {
error.message = error.response.data.message
}
return Promise.reject(error)
},
)
The idea for this post came from a lesson on the React hooks workshop from epicreact.dev. Thanks for reading!
Top comments (3)
Consider using my npm module
react-badly
😊I'd also suggest writing to the console with a little technical clue that might help on a support call --- more than you would display in the UI to a user but less than what a hacker would wish to see. dev.to/frankfont/oops-major-broker...
Thank you for writing this, Leandro! Keep up the great work.