The grass is rarely greener, but it's always different

Graceful error handling in redux asynchronous actions

For React applications I often use Redux as my state management library of choice because of its simplicity, stability, added plugins, and predictability. I find it easier to reason about data flow if I imagine it as a set of snapshots that are computed from the previous one depending on the actions the user or the business logic takes. That notion of immutability has had a beneficial impact on the way I construct and debug my programs.

With that in mind, it often happens that I want to execute an asynchronous action (such as making a call to an external API) and then update the store's state as the result of the operation.

However, as explained in their official documentation, a Redux store doesn't know anything about async logic. It knows how to dispatch actions, update the state by applying the pertinent reducer and notify the UI about the changes.

If we want to execute an asynchronous operation and update the store as a result, we can make use of redux-thunk. It is a thunk middleware for Redux that allows to easily include "delayed" operations within an action.

Its configuration is quite simple. Assuming you've installed it via npm install redux-thunk, we just need to apply the thunk middleware while creating the store:

import { createStore, applyMiddleware } from 'redux'
import thunk from 'redux-thunk'
import rootReducer from './reducers/index'

const store = createStore(rootReducer, applyMiddleware(thunk))

And then create actions by having a function that receives the parameters we might need in the execution and return a function that accepts a dispatch object:

const syncActionCreator = (value) => ({
    type: SYNC_OPERATION,
    payload: {
        value
    }
})


const asyncAction = (...args) => async (dispatch) => {
    const value1 = await doAsyncStuff();
    const value2 = await doMoreAsyncStuff(value1);

    dispatch(syncActionCreator(value2));
}

And that's it. With those small changes, we can add as much asynchronous logic as we want in our actions.

Problem

By writing actions in several web applications I used to repeat the same structure of completion:

  1. Retrieve the result of an async operation inside a try/catch block.
  2. Update the store dispatching a synchronous action with the previous result.
  3. Call a notification service by dispatching another action to communicate the success of the operation.
  4. Optionally execute a final callback with custom data calculated in the body of the action.
  5. If the async operation failed: 5.1. Parse the error the best we can. 5.2. Call the notification service communicating the failure.
  6. Optionally execute a failure callback with the error object and some custom arguments.

Solution

This way, I decided to write a generic enough small utility to help me to it:

export const tryCatchWrapper = (
    functionLogic,
    failureLogic,
    successCallback = () => {},
    notifyServiceAction = () => {}) => {

    return (...args) => async (dispatch) => {
        try {
            await functionLogic(dispatch, ...args)
            successCallback();
        } catch (error) {
            const fullError = getFullError(error);
            const notificationMessage = isObject(fullError) ?
                Object.values(fullError).map(err => err.msg).join(', ') :
                fullError;

            dispatch(
                notifyServiceAction(
                    notificationMessage,
                    FAILURE
                ));

            if (error && error.response && error.response.status === 401) {
                AuthService.logout();
                window.location.reload();
            }

            await failureLogic(dispatch, error, ...args);
        }
    }
}

Then an example of use of this code in our Websie platform is:

export const setupPatient = tryCatchWrapper(
  async (dispatch, user) => {
    const data = await ChatService.getPatientPsy(user.patientId);

    dispatch({
      type: SET_PATIENT,
      payload: { ...user }
    });

    dispatch({
        type: SET_PSY,
        payload: { ...data },
    });

  },
  async (dispatch, error) => {
    dispatch({
      type: SET_PSY_FAILURE,
    });
  }
);

Have fun!

#javascript #programming #react #redux #web