We formulate a strategy for working with errors in React

How to make the fall soft?







I did not find a comprehensive guide to error handling in React applications, so I decided to share the experience gained in this article. The article is intended for beginner developers and can be some starting point for systematizing error handling in the application.



Issues and Goal Setting



Monday morning, you calmly drink coffee and boast that you have fixed more bugs than last week and then the manager comes running and waves his hands - β€œour food has fallen off, everything is very sad, we are losing money”. You run and open your Mac, go to the production version of your SPA, make a couple of clicks to play the bug, see the white screen and only the Almighty knows what happened there, climb into the console, start digging, inside the component t there is a component with the speaking name b, in which the error cannot read property getId of undefined. N hours of research and you rush with a victorious cry to roll hotfix. Such raids occur with some frequency and have become the norm, but what if I say that everything can be different? How to reduce the time for debugging errors and build the process so that the client practically does not notice miscalculations during development that are inevitable?



Let us examine in order the problems that we have encountered:



  1. Even if the error is insignificant or localized within the module, in any case the whole application becomes inoperative

    Prior to React version 16, developers did not have a single standard error trapping mechanism, and there were situations when data corruption led to a drop in rendering only in the next steps or strange application behavior. Each developer handled errors because he was used to it, and the imperative model with try / catch generally did not fit well with the declarative principles of React. In version 16, the Error Boundaries tool appeared, which tried to solve these problems, we will consider how to apply it.
  2. The error is reproduced only in the production environment or cannot be reproduced without additional data.

    In an ideal world, the development environment is the same as production, and we can reproduce any bug locally, but we live in the real world. There are no debugging tools on the combat system. It is difficult and not productive to unearth such incidents, basically you have to deal with obfuscated code and the lack of information about the error, and not with the essence of the problem. We will not consider the issue of how to approximate the conditions of the development environment to the conditions of production, however, we will consider tools that allow you to get detailed information about incidents that have occurred.


All this reduces the speed of development and user loyalty to the software product, so I set for myself the 3 most important goals:



  1. Improve the user experience with the application in case of errors;
  2. Reduce the time between the error getting into production and its detection;
  3. Speed ​​up the process of finding and debugging problems in the application for the developer.


What tasks need to be solved?



  1. Handle critical errors with Error Boundary

    To improve the user experience with the application, we must intercept critical UI errors and process them. In the case where the application consists of independent components, such a strategy will allow the user to work with the rest of the system. We can also try to take steps to restore the application after a crash, if possible.

  2. Save extended error information

    If an error occurs, send debugging information to the monitoring server, which will filter, store and display information about incidents. This will help us quickly detect and easily debug errors after the deployment.



Critical error handling



Starting with version 16, React has changed the standard error handling behavior. Now, exceptions that were not caught using Error Boundary will lead to unmounting the entire React tree and, as a result, to the inoperability of the entire application. This decision is argued by the fact that it is better not to show anything than to give the user the opportunity to get an unpredictable result. You can read more in the official React documentation .







Also, many people are confused by the note that Error Boundary does not catch errors from event handlers and asynchronous code, however, if you think about it, any handler can ultimately change the state, based on which a new render cycle will be called, which, ultimately account may lead to an error in the UI code. Otherwise, this is not a critical error for the UI and it can be handled in a specific way inside the handler.



From our point of view, a critical error is an exception that occurred inside the UI code and if it is not processed, then the entire React tree will be unmounted. The remaining errors are not critical and can be processed according to the application logic, for example, using notifications.



In this article, we will focus on handling critical errors, despite the fact that non-critical errors can also lead to interface inoperability in the worst case. Their processing is difficult to separate into a common block and each individual case requires a decision, depending on the application logic.



In general, non-critical errors can be very critical (such a pun), so information about them should be logged in the same way as for critical ones.



Now we are designing Error Boundary for our simple application, it will consist of a navigation bar, a header and a main workspace. It is simple enough to focus only on error handling, but it has a typical structure for many applications.







We have a navigation panel of 3 links, each of which leads to independent from each other components, so we want to achieve such a behavior that even if one of the components does not work, we can continue to work with the rest.



As a result, we will have ErrorBoundary for each component that can be accessed through the navigation menu and the general ErrorBoundary, which informs about the crash of the entire application, in the event that an error occurred in the header component, nav panel or inside ErrorBoundary, but we did not solve it process and discard further.



Consider listing an entire application that is wrapped in ErrorBoundary



const AppWithBoundary = () => (   <ErrorBoundary errorMessage="Application has crashed">     <App/>   </ErrorBoundary> )
      
      





 function App() {  return (    <Router>      <Layout>        <Sider width={200}>          <SideNavigation />        </Sider>        <Layout>          <Header>            <ActionPanel />          </Header>          <Content>            <Switch>              <Route path="/link1">                <Page1                  title="Link 1 content page"                  errorMessage="Page for link 1 crashed"                />              </Route>              <Route path="/link2">                <Page2                  title="Link 2 content page"                  errorMessage="Page for link 2 crashed"                />              </Route>              <Route path="/link3">                <Page3                  title="Link 3 content page"                  errorMessage="Page for link 3 crashed"                />              </Route>              <Route path="/">                <MainPage                  title="Main page"                  errorMessage="Only main page crashed"                />              </Route>            </Switch>          </Content>        </Layout>      </Layout>    </Router>  ); }
      
      





There is no magic in ErrorBoundary, it is just a class component in which the componentDidCatch method is defined, that is, any component can be made ErrorBoundary, if you define this method in it.



 class ErrorBoundary extends React.Component {  state = {    hasError: false,  }  componentDidCatch(error) {    //            this.setState({ hasError: true });  }  render() {    if (this.state.hasError) {      return (        <Result          status="warning"          title={this.props.errorMessage}          extra={            <Button type="primary" key="console">              Some action to recover            </Button>          }  />      );    }    return this.props.children;  } };
      
      





This is what ErrorBoundary looks like for the Page component, which will be rendered into the Content block:



 const PageBody = ({ title }) => (  <Content title={title}>    <Empty className="content-empty" />  </Content> ); const MainPage = ({ errorMessage, title }) => (  <ErrorBoundary errorMessage={errorMessage}>    <PageBody title={title} />  </ErrorBoundary>
      
      





Since ErrorBoundary is a regular React component, we can use the same ErrorBoundary component to wrap each page in its own handler, simply passing different parameters to ErrorBoundary, since these are different instances of the class, their state will not depend on each other .



IMPORTANT: ErrorBoundary can catch errors only in components that are below it in the tree.



In the listing below, the error will not be intercepted by the local ErrorBoundary, but will be thrown and intercepted by the handler above the tree:



 const Page = ({ errorMessage }) => (  <ErrorBoundary errorMessage={errorMessage}>    {null.toString()}  </ErrorBoundary> );
      
      





And here the error is caught by the local ErrorBoundary:



 const ErrorProneComponent = () => null.toString(); const Page = ({ errorMessage }) => (  <ErrorBoundary errorMessage={errorMessage}>    <ErrorProneComponent />  </ErrorBoundary> );
      
      





Having wrapped each separate component in our ErrorBoundary, we achieved the necessary behavior, put the deliberately erroneous code into the component using link3 and see what happens. We intentionally forget to pass the steps parameter:



 const PageBody = ({ title, steps }) => (  <Content title={title}>    <Steps current={2} direction="vertical">      {steps.map(({ title, description }) => (<Step title={title} description={description} />))}    </Steps>  </Content> ); const Page = ({ errorMessage, title }) => (  <ErrorBoundary errorMessage={errorMessage}>    <PageBody title={title} />  </ErrorBoundary> );
      
      









The application will inform us that an error has occurred, but it will not completely fall, we can navigate through the navigation menu and work with other sections.







Such a simple configuration allows us to easily achieve our goal, but in practice, few people pay much attention to error handling, planning only the regular execution of the application.



Saving Error Information



Now that we have placed enough ErrorBoundary in our application, it is necessary to save information about errors in order to detect and correct them as quickly as possible. The easiest way is to use SaaS services, such as Sentry or Rollbar. They have very similar functionality, so you can use any error monitoring service.



I will show a basic example on Sentry, since in just a minute you can get minimal functionality. At the same time, Sentry itself catches exceptions and even modifies console.log to get all the error information. After which all errors that will occur in the application will be sent and stored on the server. Sentry has mechanisms for filtering events, obfuscating personal data, linking to releases, and much more. We will consider only the basic integration scenario.



To connect, you must register on their official website and go through the quick start guide, which will immediately direct you after registration.



In our application, we add only a couple of lines and everything takes off.



 import * as Sentry from '@sentry/browser'; Sentry.init({dsn: β€œhttps://12345f@sentry.io/12345”});
      
      





Again, click on the link / link3 in our application and get the error screen, after which we go to the sentry interface, apparently that an event has occurred and fail inside.







Errors are automatically grouped by type, frequency and time of occurrence; various filters can be applied. We have one event - we fall into it and on the next screen we see a bunch of useful information, for example stack trace







and the last user action before the error (breadcrumbs).







Even with such a simple configuration, we can accumulate and analyze error information and use it for further debugging. In this example, an error is sent from the client in development mode, so we can observe complete information about the component and errors. In order to get similar information from production mode, you need to additionally configure the synchronization of release data with Sentry, which will store the sourcemap in itself, thus allowing you to save enough information without increasing the size of the bundle. We will not consider such a configuration in the framework of this article, but I will try to talk about the pitfalls of such a solution in a separate article after its implementation.



Total:



Error handling using ErrorBoundary allows us to smooth out corners with a partial crash of the application, thereby increasing the convenience of users of the system, and the use of specialized error monitoring systems - to reduce the time of detection and debugging of problems.



Carefully think over a strategy for processing and monitoring the errors of your application, in the future this will save you a lot of time and effort. A well-thought-out strategy will primarily improve the process of working with incidents, and only then will it affect the structure of the code.



PS You can try various ErrorBoundary configuration options or connect Sentry to the application yourself in the feature_sentry branch, replacing the keys with the ones you received when registering on the site.



Git-hub demo application

React's official Error Boundary documentation



All Articles