Patrick Desjardins Blog
Patrick Desjardins picture from a conference

Handling Unexpected Error with React and Redux

Posted on: 2018-09-04

Regardless of how professional developer you are, there always will be an exception that will slip between your fingers, or should I say between your lines of code. These errors that bubble up the stack are called unexpected error. While it is always preferable to catch the error as close as possible of where the exception is thrown, it is still better to catch them at a high level. In the web world, it gives an opportunity to have a generic message to the user gracefully. It also provides an open door to collect the unexpected error and act before having any of your customers reach you.

There are three places where you need to handle unexpected errors in a stack using React and Redux. The first one is at React level. An unexpected error can occur in a render method for example. The second level is during the mapping between Redux and React. The error occurs when we move data from the Redux's store to the React's property of the connected component. The third level is an error in the chain of middlewares. The last one will bubble up through the stack of middleware and explode where the action was dispatched. Let's see how we can handle these three cases in your application.

React Unhandled Exception

Since version 16, React simplified the capture of error by introducing the lifecycle function componentDidCatch. It is a function like render or componentShouldUpdate that come with the framework. The componentDidCatch is triggered when an exception go thrown any children of the component. The detail about what it covers is crucial. You must have a component that englobes most of your application. If you are using React-Router and would like to keep the web application with the same URL and have the menus to stay in place, this can be tricky. The idea is to create a new component with the sole purpose of wrapping all top route components. Having a single component to handle the unexpected error is interesting. It is simple, easy to test, with a cohesive and single task.

export interface ErrorBoundaryStates { hasError: boolean; } 
export class ErrorBoundary extends React.Component<ErrorBoundaryProps, ErrorBoundaryStates> { 
  constructor(props: ErrorBoundaryProps) { 
    super(props); 
    this.state = { hasError: false }; 
  }

  public componentDidCatch(error: Error, errorInfo: ErrorInfo): void { 
    this.setState({ hasError: true }); 
    YourLoggingSystem.track(error, "ErrorBoundary", { errorInfo: errorInfo }); 
  }

  public render() { 
    if (this.state.hasError) { 
      return <div className="ErrorBoundary"> The application crashed. Sorry! </div>; 
    } 
    return this.props.children; 
  } 
} 

However, with React-Router, every route is assigned as property. The property is an excellent opportunity to create a function that returns the React class.

// CODE IN YOUR TOP APP CONTAINER:

// Constructor: 
this.page1Component = withErrorBoundary()(Page1Component)); 

// Render: 
<Route path={Routes.PAGE_1} component={this.page1Component} />

// UTILITY FUNCTION 
export const withErrorBoundary = () => <P extends object>(Component: React.ComponentType<P>) 
  => class WithErrorBoundary extends React.Component<P> { 
    render() { 
      return <ErrorBoundary}><Component {...this.props} /></ErrorBoundary>; 
    } 
}; 

Redux Mapping Unhandled Exception

This section will be short because it is covered with React. However, I wanted to clarify that this can be tricky if you are not doing exactly like the pattern I described. For instance, if you are wrapping the withErrorBoundary not at the initialization of the route but directly when you connect -- it will not work. For example, the code below does not work as you might expect. The reason is that the error is bound to the component but not to the code being bound by the React-Connect.

export default connect<ModelPage1, DispatchPage1, {}, {}, AppReduxState>( (s) => 
  orgMappers.mapStateToProps(s), (s) => orgMappers.mapDispatchToProps(s) )(WithErrorBoundary()(Page1Component)); 

Outside of the initial solution proposed, it is also valid to wrap the connect to have the desired effect of receiving the error in the componentDidCatch of the ErrorBoundary. I prefer the former solution because it does not coerce the ErrorBoundary with the component forever.

export default WithErrorBoundary(connect<ModelPage1, DispatchPage1, {}, {}, AppReduxState>( (s) => 
  orgMappers.mapStateToProps(s), (s) => orgMappers.mapDispatchToProps(s) )(Page1Component)); 

Redux Middleware Unhandled Exception

The last portion of the code that needs to have a catch-all is the middleware. The solution goes with the Redux's middleware concept which is to leverage function that calls themselves. The idea is to have one of the first function, middleware, to be a big try-catch.

const appliedMiddleware = applyMiddleware( thunk, unhandledExceptionMiddleware, middleware1, middleware2, middleware3, middleware4 );

// Excerpt of the middleware: return (api: MiddlewareAPI<Dispatch, AppReduxState>) => 
  (next: Dispatch) => 
    (action: Actions): any => { 
      try { 
        return next(action); 
      } 
      catch (error) { 
        YourLoggingSystem.track(error, "Unhandled Exception in Middleware"); 
        return next(/*Insert here an action that will render something to the UI to indicate to the user that an error occured*/); 
      } 
    }; 

Summary

Handling errors in a React and Redux world require code to be in a particular way. At this day, the documentation is not very clear. Mostly because there is a clear separation between React, Redux, Redux-Connect, and React-Router. While it is very powerful to have each element of the puzzle separated, this comes with the price that the integration is in a gray area. Hopefully, this article uncovers some mystery around how to collection unhandled errors and removed confusion with the particular case of why mapping error can throw error through the React mechanism when not positioned at the right place.