Hello readerš
In this blog post, we will give an overview on why catching and handling errors is important in software engineering in general and the ways offered by popular web development tools such as React to effectively handle errors.
The majority of the well known applications we use on a daily basis, like Facebook, YouTube, etc.,
are extremely huge, sophisticated and very often made by several people (can be more than 100 engineers for very big products). Very often, despite having many automated systems that are checking for errors before they reach production, there are very often errors in the code of those products that makes it crash for some specific situations. It's not that the developers who created those products are stupid, lazy or incompetent, it's just that, in the rush to meet a deadline, itās difficult to foresee everything a user could do to the system they are working on which often leads to the software crashing in some use cases.
Because of the importance of software programs in our current world and the criticality of the areas in which some programs are used such as the healthcare industry, it is important to effectively handle potential errors that might be in the code of those programs in order to offer the best possible product to the end users and to avoid any bad consequence of a faulty program.
It is important to handle potential errors that can be in your React app because those errors might end-up crashing the entire React lifecycle or reaching the top-level of the main execution thread. From React 16 onward, as stated in the official documentation, errors that are not caught will result in unmounting of the whole React component tree.
Because of the importance of handling errors we stated above, it is very important to handle potential issues that may happen, and eventually provide suitable feedback to the users and also to the developers so that those errors are quickly fixed.
Fortunately, implementing such patterns is simple with the recent React APIs. More specifically, React 16 introduces the Error Boundaries mechanism that is very useful for that purpose.
From React 16, all errors that occurred during the rendering phase are printed into the console in development. In addition to that, the component stack trace is also provided. We can therefore see where exactly in the component tree the error has happened:
The component stack trace additionally includes filenames and line numbers that you can use to see where exactly the error happened. The component stack trace works by default in projects created using Create React App. If you donāt use it, you can manually addĀ this pluginĀ to your Babel setup.
If you are familiar with JavaScript, you are probably familiar with the usual try ā¦ catch
block used to catch errors.
Using the usualĀ try...catch
Ā statement is not effective to catch errors in your react components because it only works for imperative code and not the declarative code we write in JSX.
Moreover, with aĀ try...catch
Ā statement, we might cause the entire react app to break instead of just the faulty component which is not what you most probably want.
Error Boundaries are the recommended way to handle potential errors in React components. According to the React documentation, āError boundaries are 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. Error boundaries catch errors during rendering, in lifecycle methods, and in constructors of the whole tree below them.ā
Not all errors will be catched with Error boundaries. Below is a list of errors that wonāt be catched using this mechanism:
setTimeout
Ā orĀ requestAnimationFrame
Ā callbacks)When using Error Boundaries, errors that might happen in a specific component wonāt make the global component tree to crash. Instead, the error propagation will stop at the Error Boundary level
Error Boundaries are implemented using class components. According to the React documentation, a class component becomes an error boundary if it defines either (or both) one of the lifecycle methodsĀ static getDerivedStateFromError
Ā orĀ componentDidCatch
.
static getDerivedStateFromError(error)
Ā is used to render a visual fallback UI after an error has been thrown. It will be called after a descendant component throws an error. This method takes the error as an argument and the value returned by this function is used to update the state.
componentDidCatch(error, info)
Ā is usually used to log error information. It will be called as soon as an error reaches our component. This method accepts two arguments:
error
Ā - The error that was throwninfo
Ā - An object storing the componentStack trace showing which component has thrown this error.As we previously mentioned, an error boundary is just a class component that implements one of those methods static getDerivedStateFromError()
Ā orĀ componentDidCatch()
or both.
Here is a simple React app where we are explicitly throwing an error in the Users component below
import { Users } from "./Users";
function App() {
return (
<div className="App">
<h1>Hello dear reader</h1>
<Users />
</div>
);
}
export default App;
Here is the Users component where we are explicitly throwing an error
export const Users = () => {
throw new Error("Error!");
return <div> Users </div>;
};
Running this React app will throw the following error and cause the entire app to crash
To prevent the app from entirely crashing, we need to add the Error Boundary component. Here is a simple implementation of an error boundary class
import React from "react";
export class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
console.error(error, errorInfo);
}
render() {
if (this.state.hasError) {
return <h1>Something went wrong.</h1>;
}
return this.props.children;
}
}
After defining this, we only need to wrap the necessary component where we might have an error inside the Error Boundary component.
import { ErrorBoundary } from "./ErrorBoundary";
function App() {
return (
<div className="App">
<h1>Hello</h1>
<ErrorBoundary>
<Users />
<ErrorBoundary />
</div>
);
}
export default App;
The app now runs but the error propagation stops at the Error Boundary component and the app doesnāt entirely crash.
If you are not satisfied with the features offered by the Error Boundary component, you can use the react-boundary-library
that provides a retry mechanism as well as a way to render a fallback component in case of an error.
# if you use npm
npm install --save react-boundary-library
# if you use yarn
yarn add react-boundary-library
Firstly, all the features offered by this library are well explained in their official repository. Weāll see here a simple example to help you get started.
To handle errors using this library, you can do it simply by wrapping the component where your error might show up inside the ErrorBoundary
component from react-boundary-library
. You can also provide a fallback component as a prop to the Error Boundary component which will be the UI shown in case of an error. Here is a concrete example
import { ErrorBoundary } from "react-error-boundary";
import { Users } from "./Users";
function ErrorFallback({ error, resetErrorBoundary }) {
return (
<div role="alert">
<p>Failed to load users:</p>
<p>{error.message}</p>
<button onClick={resetErrorBoundary}>Try again</button>
</div>
);
}
function App() {
return (
<div className="App">
<h1>Hello dear reader</h1>
<ErrorBoundary FallbackComponent={ErrorFallback}>
<Users />
</ErrorBoundary>
</div>
);
}
The Users component is the same one as before.
The Error Boundary component takes one mandatory prop which is the fallback component or JSX code to display in the case of an error.
The fallback component acceptsĀ error
Ā andĀ resetErrorBoundary
Ā as props. The resetErrorBoundary
prop will reset the error boundary's state when called, and can be useful for a "try again" button. error
is the thrown error.
Here is the output we get when running the above code
Generally speaking, a software error is essentially a mismatch between what is expected from the program and the real output. It is almost impossible to make software that is error-free, and that even with the development of automated testing tools. On some occasions, these errors have little effect, while in other contexts, like in the healthcare industry or in banking applications, the effects of an error can be extremely expensive to fix. It is therefore important to handle any potential error in the most efficient way possible.
In the case of web applications and more specifically in the case of web components, modern JavaScript frameworks usually offer ways to handle errors that might be in your web component other than just the usual try ā¦ catch
. Error Boundaries components are an effective solution offered in the React ecosystem for that purpose. This solution helps in offering visual UI feedback and stops the error propagation in order to not cause the entire app to crash. There are as well other libraries that one can use to provide an advanced error-handling experience such as react-boundary-library
in the React ecosystem.
If this post was helpful to you in any way, please don't hesitate to share it. You can also followĀ my Twitter accountĀ if you would like to see more content related to web programming.