Search code examples
reduxrxjsrxjs5reactivexredux-observable

Testing error handling for redux-observable epics


Here I'm using redux-observable and having some issues testing the error handling.

See the following logout user epic:

const logoutUserEpic = (action$) => action$
    .ofType(LOGOUT_USER)
    .mergeMap(() =>
        Observable.from(
            graphQl(AuthLogout)
                .catch((error) => failure(error))
        )
        .mergeMap(() => Observable.of(
                receive(),
                push('/login')
            )
        )
    )

Here's a test that mocks out the graphQl service to make sure that it fires the failure action when the promise rejects:

it('should return the error action', () => {
    const error = Error('fail')
    request.graphQl = () => Promise.reject(error)

    const action$ = ActionsObservable.of((logoutUser()))

    return logoutUserEpic(action$)
    .toArray()
    .forEach((actions) => {
        expect(actions).toEqual([{
            meta: { isFetching: false },
            payload: error,
            type: FAILURE
        }])
    })
})

In the test it dispatches the actions that are flattened in mergeMap rather than the expected failure action in the catch.

Is it valid to have the error handling set up like this or am I showing my noobishness with RxJs?


Solution

  • The issue is that you're catching the error on the Promise itself and then your mergeMap is ignoring the result of the Promise so you're throwing away failure(error) (presumably an error action)

    const logoutUserEpic = (action$) => action$
        .ofType(LOGOUT_USER)
        .mergeMap(() =>
            Observable.from(
                graphQl(AuthLogout)
                    .catch((error) => failure(error))
                    // If there is an error, you caught it and transformed it
                    // instead into a failure(error) down the success path
            )
            // If there was an error, it is now no longer an error and this
            // mergeMap will just throw that failure(error) away
            .mergeMap(() => Observable.of(
                    receive(),
                    push('/login')
                )
            )
        )
    

    Instead, you should catch the error using RxJS, in this case placing it after the mergeMap--note that RxJS's catch and Promise catch are similar but not the same thing!

    const logoutUserEpic = (action$) => action$
      .ofType(LOGOUT_USER)
      .mergeMap(() =>
        Observable.from(graphQl(AuthLogout))
          .mergeMap(() => Observable.of(
            receive(),
            push('/login')
          ))
          .catch((error) => Observable.of(failure(error)))
      )