Tech

Graceful Error Handling with Redux

By September 16, 2019 No Comments

Redux powers our global state at FireHydrant, one of the things we use most heavily is the ability to let redux store our API errors to handle failure states on the UI. Outside of showing error states, properly handling errors keeps your application up and running. Nothing is worse than getting a page because your application didn’t properly handle an API error response and crashed your entire React app.

What we will end up with

In this post we will build a component that wraps a version of draft-js, a rich text editor component, along with bootstrap alerts that appear when the content saves and fails to save.

We will go through the state and reducer, actions, and the component that uses the error state.

State and reducer needs

Our state needs here are simple, all we need are error and data to capture all of the states we need for this component.

const initialState = {
  error: null,
  data: null,
};

Again the reducers are very simple, we just need to modify two fields so we handle each of those states.

case UPDATE_ADDITIONAL_DETAILS_SUCCESS:
  newState = {
    data: action.data,
    error: null,
  };
  return Object.assign({}, state, newState);
case UPDATE_ADDITIONAL_DETAILS_ERROR:
  newState = {
    error: action.error,
  };
  return Object.assign({}, state, newState);

Actions

Actions are where the real meat of this logic comes into play. Since these are API calls, this action is being run with redux thunk for async dispatching.

// Additional Details
export const updateAdditionalDetails = data => dispatch => {
  return new Promise((resolve, reject) => {
    axios.patch('additional_details', { data })
      .then(response => {
        dispatch(updateAdditionalDetailsSuccess(response.data));
        resolve();
      })
      .catch(err => {
        dispatch(updateAdditionalDetailsError(err.response));
        reject();
      });
  });
};

By wrapping the axios request in a Promise, we now have the ability to call additional functions on the success or failure of the API request from the component that is calling them. This is something that has a lot of benefits for creating side effects in the parent component which we will look at in the next section. We’re also calling our traditional redux actions here. This is also the first error handling step, we’re catching the bad API response, keeping the app up and running. However, we’re rejecting that Promise so we also need to catch from the function that is calling this action to prevent an UnhandledPromiseRejectionWarning.

These are the actions interacting with the reducer.

export const UPDATE_ADDITIONAL_DETAILS_SUCCESS = 'UPDATE_ADDITIONAL_DETAILS_SUCCESS';
export const updateAdditionalDetailsSuccess = data => ({
  type: UPDATE_ADDITIONAL_DETAILS_SUCCESS,
  data,
});

export const UPDATE_ADDITIONAL_DETAILS_ERROR = 'UPDATE_ADDITIONAL_DETAILS_ERROR';
export const updateAdditionalDetailsError = error => ({
  type: UPDATE_ADDITIONAL_DETAILS_ERROR,
  error,
});

Using error state in the component

Now that we our handling our error in our store, let’s look at how this error is managed in the actual component. This Freeform component has an editor and two Alert reactstrap components wrapped in Fade components for proper animation. As the editor calls its onChange handler it dispatches the action to submit to the API. Once the axios request completes it will call fadeIn to modify the state and fade in the proper Alert and then fade it out 3 seconds later.

export class Freeform extends React.Component {
  constructor(props) {
    super(props);
    this.state = { success: false, danger: false };
  }

  fadeIn = type => {
    this.setState({ [type]: true });
    setTimeout(() => {
      this.setState({ [type]: false });
    }, 3000);
  }

  handleChange = value => {
    const { dispatchUpdateReportAdditionalDetails } = this.props;
    dispatchUpdateReportAdditionalDetails(postmortemReportId, data).then(() => this.fadeIn('success')).catch(() => this.fadeIn('danger'));
  }

  render() {
    const { error } = this.props;
    const { success, danger } = this.state;
    return (
          <Editor onChange={this.handleChange} />
          <div className="mt-3">
            <Fade in={success} mountOnEnter unmountOnExit>
              <Alert color="success">Saved</Alert>
            </Fade>
            <Fade in={danger} mountOnEnter unmountOnExit>
              <Alert color="danger">{`Failed: ${error}`}</Alert>
            </Fade>
          </div>
    );
  }
}

const mapStateToProps = state => ({
  error: state.error,
})

const mapDispatchToProps = dispatch => ({
  dispatchUpdateReportAdditionalDetails: data => dispatch(updateAdditionalDetails(data)),
});

Error handling is something that you are most likely already doing. Wrapping errors in this way let you hook into more powerful side effect generation and graceful states. Don’t force your team to wake up from a page that is caused because your application failed when a single API returned an error.

You just got paged. Now What? Get started with FireHydrant.