21
Handling errors gracefully in react with error boundaries
- Understanding of error types such as run-time, and compile-time errors.
- Knowledge of Class based components.
- An sentry account for logging errors
- Architectural design pattern for implementing error boundaries in react.
- Implementation of error boundary from scratch.
- Types of errors catched by error boundaries.
- Common problems faced during the usage of react error boundary.
- react-error-boundary to the rescue.
- Implementation of third-party error logging tools such as sentry.
- In react, all the error boundaries are made up of class based components.
- Error boundaries are some of the graceful ways using which you can catch errors in a more efficient way.
- You can consider it as a
try
andcatch
blocks of JSX ecosystem. - Below is a simple example of error boundaries in react
const App = () => {
return (
<div>
<h1>Counter Example</h1>
<ErrorBoundary fallBackUIComponent={<FallBackUI />}>
<BuggyComponent />
</ErrorBoundary>
</div>
);
}
- As you can see
ErrorBoundary
component is placed as a parent to a component which we suspect might cause an error. - Whenever a run-time error occurs in the
BuggyComponent
the nearest error boundary which isErrorBoundary
component catches it and displays a fallback UI. Below Gif will explain this scenario.
- Since the error boundary is a class based component therefore it has certain methods which it uses to catch errors. Below is the architectural diagram of the
ErrorBoundary
:
-
Before Implementing the error boundary we should keep in mind the following things:
- Error boundary is always a
class
based component. - It uses following two methods to catch the errors:
-
static getDerivedStateFromError()
: A static method which is executed before the DOM is ready(during the rendering phase of the component). This will get invoked whenever descendant component throws an error. -
componentDidCatch()
: This will get invoked whenever a descendant component throws an error. This component is called duringcommit
phase i.e. When the DOM is ready. It can be used to perform side-effects in the component. It receives two parameters:-
error
- error that is being thrown. -
info
- An object with componentStack which tells us which component threw an error.
-
-
- Error boundary is always a
Now we can move towards the implementation of the error boundary. Below code will demonstrate a class based react error boundary:
class ErrorBoundary extends React.Component {
constructor(props){
super(props);
this.state = {
hasError: false
};
}
static getDerivedStateFromError(error) {
// Update state so the next render will show the fallback UI.
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
//Can be used to log to any logging service like sentry
console.log("Catched error", errorInfo);
}
render(){
if(this.state.hasError){
return(
// <h3>Something went wrong!</h3>
//Can be a static or a fall-back component passed as a prop.
this.props.fallBackUIComponent
);
}
return this.props.children;
}
}
Few things to note in the above implementation:
-
getDerivedStateFromError
returns a value to update the state of the component in this casehasError
is set to true. -
componentDidCatch
will also catch the error along with the stack trace of the error. This will occur on the commit phase of the component.
Lastly in the render
function if the hasError
state is true
then this will print our fallback component which we passed it as a prop. Else it will return the children
.
Usage of this component is fairly simple. Just wrap the compontent in the question with the ErrorBoundary
Component so that it catches the error thrown by it's descendant. Below example will give you a clear idea of it's usage:
//Component for fallback UI:
const FallBackUI = () => {
return (
<>
<h3>Something went wrong</h3>
</>
);
}
const BuggyComponent = () => {
const [count, setCount] = React.useState(0);
const increaseCounter = () => {
setCount(preVal => preVal + 1);
}
if(count === 5) {
throw new Error("Crashing the app!!");
}
return (
<>
<div className="counter--block">
<span>Counter</span>
<span>{count}</span>
</div>
<button onClick={increaseCounter}>Increase count</button>
</>
);
}
const App = () => {
return (
<div>
<h1>Counter Example</h1>
<ErrorBoundary fallBackUIComponent={<FallBackUI />}>
<BuggyComponent />
</ErrorBoundary>
</div>
);
}
ReactDOM.render(
<App />
,
document.getElementById("root")
);
- React's Error Boundary documentation clearly states that it catches only the errors which occur during the life-cycle of a component i.e. It will catch only run-time errors.
-
Below mentioned errors are not being catched by react's error boundaries:
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)
There might be couple of reasons for error boundary
not to work.
Some of them are mentioned below:
There are some cases where we forget that the component needs to be always wrapped with the ErrorBoundary
component so that it catches error. Below example will provide clear understanding:
Consider a component which will throw an error when the counter value reaches 5
:
const BuggyComponent = () => {
const [count, setCount] = React.useState(0);
const increaseCounter = () => {
setCount(preVal => preVal + 1);
}
if(count === 5) {
throw new Error("Crashing the app!!");
}
return (
<>
<div className="counter--block">
<span>Counter</span>
<span>{count}</span>
</div>
<button onClick={increaseCounter}>Increase count</button>
</>
);
}
Placing the error boundary like below will never allow the ErrorBoundary
Component to catch error, since the BuggyComponent
is not being wrapped with ErrorBoundary
but rather the content of this component is wrapped with ErrorBoundary
.
return (
<ErrorBoundary>
<div className="counter--block">
<span>Counter</span>
<span>{count}</span>
</div>
<button onClick={increaseCounter}>Increase count</button>
</ErrorBoundary>
);
And also neither any of this will capture the error throw by BuggyComponent
. To make this work we can do something like this:
const App = () => {
return (
<div>
<h1>Counter Example</h1>
<ErrorBoundary>
<BuggyComponent />
</ErrorBoundary>
</div>
);
}
Now the ErrorBoundary
will catch the error thrown by the BuggyComponent
since it is being wrapped by the error boundary.
In the above usecase as you have seen whenever the count value reaches 5 it will throw a new error.
Note: The if
block for this is placed in the rendering phase of the component because of which it creates a valid case for ErrorBoundary
to catch the error.
const BuggyComponent = () => {
const [count, setCount] = React.useState(0);
const increaseCounter = () => {
setCount(preVal => preVal + 1);
}
if(count === 5) {
throw new Error("Crashing the app!!");
}
return (
<>
<div className="counter--block">
<span>Counter</span>
<span>{count}</span>
</div>
<button onClick={increaseCounter}>Increase count</button>
</>
);
}
const App = () => {
return (
<div>
<h1>Counter Example</h1>
<ErrorBoundary>
<BuggyComponent />
</ErrorBoundary>
</div>
);
}
But the same won't work if you place the if
block inside the increaseCounter
function. The above example is altered to showcase this scenario:
const BuggyComponent = () => {
const [count, setCount] = React.useState(0);
const increaseCounter = () => {
setCount(preVal => preVal + 1);
if(count === 5) {
throw new Error("Crashing the app!!");
}
}
return (
<>
<div className="counter--block">
<span>Counter</span>
<span>{count}</span>
</div>
<button onClick={increaseCounter}>Increase count</button>
</>
);
}
const App = () => {
return (
<div>
<h1>Counter Example</h1>
<ErrorBoundary>
<BuggyComponent />
</ErrorBoundary>
</div>
);
}
react-error-boundary
is a pretty impressive package. It solves most of the challenges faced by react's error boundary where it won't be able to catch errors such as errors thrown from event handlers, asynchornous code etc.
You can refer to the package's github readme for more information.
Below is the implmentation of the above example but using react-error-boundary
:
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>
)
}
const BuggyCounter = () => {
const [count, setCount] = React.useState(0);
const handleIncrement = () => {
setCount(preVal => preVal + 1);
}
if(count === 5){
throw new Error("New Crashing Seq. Initiated");
}
return(
<div className="counter--block">
<span>Count</span>
<span>{count}</span>
<button onClick={handleIncrement}>Increment count</button>
</div>
);
}
const App = () => {
return(
<>
<h1>Counter Example</h1>
<ErrorBoundary FallbackComponent={ErrorFallback}>
<BuggyCounter />
</ErrorBoundary>
</>
)
}
ReactDOM.render(
<App/>,
document.getElementById("root")
);
Error logging is a crucial part of any application development process. It helps us to analyze and organize errors which are not catched during the testing process of the application. These error logging tools can generally be used to moniter the errors which are thrown on the client's machine/browser.
When it comes to error logging I find sentry.io to be a bliss. It has pretty impressive documentation and has wide range of support on different tech stacks such as Java, JS, React, React-Native etc.
Below is the modified example of the above example.
import React from "react";
import ReactDOM from "react-dom";
import * as Sentry from "@sentry/react";
import App from "./App";
Sentry.init({ dsn: "https://[email protected]/0" });
const BuggyCounter = () => {
const [counter, setCounter] = useState(0);
return (
<>
<div className="counter--value">
{counter}
</div>
<div>
<button
className="counter--button"
onClick={() => { throw new Error("New Test Error")}}>
increment count
</button>
</div>
</>
)
}
const App = () => {
return (
<Sentry.ErrorBoundary fallback={"An error has occurred"}>
<BuggyCounter />
</Sentry.ErrorBoundary>
);
}
ReactDOM.render(<App />, document.getElementById("root"));
// Can also use with React Concurrent Mode
// ReactDOM.createRoot(document.getElementById('root')).render(<App />);
In this example you need to first initialize the Sentry's instance with init function:
Sentry.init({ dsn: "https://[email protected]/0" });
NOTE: dsn
is data source name which tells the SDK where to send the events.
Sentry also provides it's own error boundary component.
import * as Sentry from "@sentry/react";
const App = () => {
return (
<Sentry.ErrorBoundary fallback={"An error has occurred"}>
<BuggyCounter />
</Sentry.ErrorBoundary>
);
}
You can find the code used in this blogpost below:
Implementation of react error boundary from scratch:
https://codepen.io/keyurparalkar/pen/LYWJKvm?editors=0010Implementation of react error boundary using
react-error-boundary
package:
https://codepen.io/keyurparalkar/pen/bGqQNJe
Feel free to reach out to me @
21