What is the best way to deal with a fetch error in react redux?

The approach I'm currently taking for a few specific errors (user input validation) is to have my sub-reducers throw an exception, catch it in my root reducer, and attach it to the action object. Then I have a redux-saga that inspects action objects for an error and update the state tree with error data in that case.

So:

function rootReducer(state, action) {
  try {
    // sub-reducer(s)
    state = someOtherReducer(state,action);
  } catch (e) {
    action.error = e;
  }
  return state;
}

// and then in the saga, registered to take every action:
function *errorHandler(action) {
  if (action.error) {
     yield put(errorActionCreator(error));
  }
}

And then adding the error to the state tree is as Erik describes.

I use it pretty sparingly, but it keeps me from having to duplicate logic which legitimately belongs in the reducer (so it can protect itself from an invalid state).


Erik’s answer is correct but I would like to add that you don’t have to fire separate actions for adding errors. An alternative approach is to have a reducer that handles any action with an error field. This is a matter of personal choice and convention.

For example, from Redux real-world example that has error handling:

// Updates error message to notify about the failed fetches.
function errorMessage(state = null, action) {
  const { type, error } = action

  if (type === ActionTypes.RESET_ERROR_MESSAGE) {
    return null
  } else if (error) {
    return error
  }

  return state
}

write custom Middleware to handle all the api related error. In this case your code will be more cleaner.

   failure/ error actin type ACTION_ERROR

   export default  (state) => (next) => (action) => {

      if(ACTION_ERROR.contains('_ERROR')){

       // fire error action
        store.dispatch(serviceError());

       }
}

If you want to have the concept of "global errors", you can create an errors reducer, which can listen for addError, removeError, etc... actions. Then, you can hook into your Redux state tree at state.errors and display them wherever appropriate.

There are a number of ways you could approach this, but the general idea is that global errors/messages would merit their own reducer to live completely separate from <Clients />/<AppToolbar />. Of course if either of these components needs access to errors you could pass errors down to them as a prop wherever needed.

Update: Code Example

Here is one example of what it might look like if you were to pass the "global errors" errors into your top level <App /> and conditionally render it (if there are errors present). Using react-redux's connect to hook up your <App /> component to some data.

// App.js
// Display "global errors" when they are present
function App({errors}) {
  return (
    <div>
      {errors && 
        <UserErrors errors={errors} />
      }
      <AppToolbar />
      <Clients />
    </div>
  )
}

// Hook up App to be a container (react-redux)
export default connect(
  state => ({
    errors: state.errors,
  })
)(App);

And as far as the action creator is concerned, it would dispatch (redux-thunk) success failure according to the response

export function fetchSomeResources() {
  return dispatch => {
    // Async action is starting...
    dispatch({type: FETCH_RESOURCES});

    someHttpClient.get('/resources')

      // Async action succeeded...
      .then(res => {
        dispatch({type: FETCH_RESOURCES_SUCCESS, data: res.body});
      })

      // Async action failed...
      .catch(err => {
        // Dispatch specific "some resources failed" if needed...
        dispatch({type: FETCH_RESOURCES_FAIL});

        // Dispatch the generic "global errors" action
        // This is what makes its way into state.errors
        dispatch({type: ADD_ERROR, error: err});
      });
  };
}

While your reducer could simply manage an array of errors, adding/removing entries appropriately.

function errors(state = [], action) {
  switch (action.type) {

    case ADD_ERROR:
      return state.concat([action.error]);

    case REMOVE_ERROR:
      return state.filter((error, i) => i !== action.index);

    default:
      return state;
  }
}