Redux saga error handling with multiple API calls

Viewed 4194

I had two thunks, that I am trying to convert to a single saga. When the user logs into their dashboard, they make two API calls, one for projects and the other for notifications. In the thunks, following Flux standard actions invoking the errors for each was trivial, but I am unsure what the best approach is for doing this with sagas. This is what I have to start from:

export function* loadDashboard() {
  try {
    yield put({ type: types.notificationTypes.GET_NOTIFICATIONS_REQUEST });
    const [projects, notifications] = yield all([
      DashboardService.getProjects,
      DashboardService.getNotifications
    ]);
    yield put({
      type: types.projectTypes.GET_PROJECTS_SUCCESS,
      payload: projects
    });
    yield put({
      type: types.notificationTypes.GET_NOTIFICATIONS_SUCCESS,
      payload: notifications
    });
  } catch (error) {
    //My guess is both of these will render the same error to their reducer, 
    //regardless of who the originator is
    yield put({
      type: types.projectTypes.GET_PROJECTS_FAILURE,
      error: error.toString()
    });
    yield put({
      type: types.notificationTypes.GET_NOTIFICATIONS_FAILURE,
      error: error.toString()
    });
  }
}
2 Answers

I ended up combining all() with spawn():

export function* loadDashboard() {
  yield all([
    spawn(projectActions.getProjects),
    spawn(notificationActions.getNotifications),
    spawn(teamActions.getTeams)
  ]);
}

From redux saga docs:

"Detached forks (using spawn) Detached forks live in their own execution context. A parent doesn't wait for detached forks to terminate. Uncaught errors from spawned tasks are not bubbled up to the parent. And cancelling a parent doesn't automatically cancel detached forks (you need to cancel them explicitly).

In short, detached forks behave like root Sagas started directly using the middleware.run API."

I manually tested this by excluding auth headers from one GET request out of 3 total, and the error propogated individually while the others proceeded to success. I ran to test that the SUCCESS calls arrived AFTER the failure just to make sure. However, the behavior seemed functionally equivalent when writing the same loadDashboard() method with 3 fork() instead of 3 spawn() calls. Will need to write proper tests to figure out which implementation is ideal per this scenario.

EDIT: After using/implementing saga composition over the past few months I believe that using fork is best when you want to be able to cancel() child processes and spawn is when you do not care about task cancellation.

I think it's better to reparate logic of the two API requests.

This improves readability and maintainability:

export function* loadDashboard() {
  yield all([loadProjects, loadNotifications])
}

function* loadProjects() {
  try {
    yield put({ type: types.GET_PROJECTS_REQUEST })
    const projects = yield call(DashboardService.getProjects)
    yield put({
      type: types.projectTypes.GET_PROJECTS_SUCCESS,
      payload: projects
    })
  } catch (error) {
    yield put({
      type: types.projectTypes.GET_PROJECTS_FAILURE,
      error: error.toString()
    })
  }
}

function* loadNotifications() {
  try {
    yield put({ type: types.notificationTypes.GET_NOTIFICATIONS_REQUEST });
    const notifications = yield call(DashboardService.getNotifications)
    yield put({
      type: types.notificationTypes.GET_NOTIFICATIONS_SUCCESS,
      payload: notifications
    })
  } catch (error) {
    yield put({
      type: types.notificationTypes.GET_NOTIFICATIONS_FAILURE,
      error: error.toString()
    })
  }
}

This code is a bit different, though.

Redux-saga's all() is actually all-or-nothing: if one of the tasks throws an error, all tasks that are stil running are cancelled.

I don't think this is what you want, so in my code I prevent this by catching errors per request.

Task cancellation and cancellation propagation are the primary differences of sagas from promises. These things often work by default, so take your time to understand them.

Related