redux-observable: Add error handling examples

Hi,

I think it’s not clear right now how to handle errors with this middleware. There is no example of it in the basic example nor the medium article.

Lets say I have this action creator:

function createRequestAction({request, success, failure, abort}, fetch) {
    return payload => actions => Rx.Observable
            .fromPromise(fetch(payload))
            .map(success)
            .takeUntil(actions.ofType(abort.toString()))
            .startWith(request());
}

const getUser = createRequestAction(userActions, fetchUser);

// ...
store.dispatch(getUser({id: 123}));

If the promise returned by fetchUser fail, it will just throw an error and I don’t see any way to handle it in a proper RxJS fashion. In RxJS v4 there was a catch method, but now there isn’t. How should i properly dispatch the failure error when the request fail?

About this issue

  • Original URL
  • State: closed
  • Created 8 years ago
  • Comments: 22 (9 by maintainers)

Most upvoted comments

catch does actually exist in rxjs v5. import 'rxjs/add/operator/catch';

Here’s an (untested) example:


const fetchUser = (userId) => (
  (actions, store) => (
    // Promises aren't truly cancelable, use Observable.ajax
    // unless you *really* need to consume a Promise
    Observable.ajax(`/api/users/${userId}`)
      .map(payload => ({ type: 'FETCH_USER_FULFILLED', payload }))
      .takeUntil(actions.ofType('FETCH_USER_ABORTED'))
      // If the ajax request errors, we'll catch it and instead emit a
      // "FETCH_USER_ERRORED" action with the error info, your
      // reducer would then be responsible for returning the correct state
      .catch(error => Observable.of({ type: 'FETCH_USER_ERRORED', error }))
      .startWith({ type: 'FETCH_USER_PENDING' });
  )
);

There’s also onErrorResumeNext, which was just recently added in rxjs@v5 master, it isn’t yet in any beta release.

We do need to add usage examples for this here, I agree. I’m going to be adding a real API to the examples so we can do real Observable.ajax() and demo error handling too.

You’re right - I need to sleep

One way to get what you want is to do .flatMap(action => post().map().catch()) instead of .flatMap(action => post()).map().catch().

@matthewwithanm Depends on whether you want the Epics that errored to resume listening for actions. If yes, this works:

http://jsbin.com/wubeqa/edit?js,output

const rootEpic = (action$, store) =>
  combineEpics(firstEpic, secondEpic)(action$, store)
    .catch((error, stream) => {
      // DO SOMETHING WITH THE ERROR HERE
      console.error('Uncaught', error.stack);
      // resume with the errored observable instead of just terminating
      return stream;
    });

If you wish to just let things terminate, you’d use .do():

const rootEpic = (action$, store) =>
  combineEpics(firstEpic, secondEpic)(action$, store)
    .do({
      error: error => console.error('ERROR: ', error)
    });

Composition is one of the major reasons we chose the Epic pattern. But…because an uncaught exception will cause all the combined Epic streams to terminate, we’ve considered making the first example effectively the default of what the middleware does–so that all Epics are not impacted by one misbehaving Epic. But this is potentially too opinionated, so at the very least we should mention and document this pattern.

@SethDavenport hmmm AFAIK .retry() doesn’t accept a function like that (only a number of retry attempts). Not sure how that’s working 😆

You want to catch the error from the the ajax observable to not let it propagate up to the outermost one.

http://jsbin.com/boqaso/edit?js,output

const fetchBadURLEpic = action$ =>
  action$.ofType(FETCH_BAD_URL)
    .flatMap(action =>
      Observable.ajax('http://example.com/i-will-fail')
        .mapTo({ type: FETCH_BAD_URL_FULFILLED })
        .catch(error => Observable.of({
          type: FETCH_BAD_URL_REJECTED,
          payload: error,
          error: true
        }))
    );

I’m trying to understand error handling in the context of an epic:

loginEpic = (action$: ActionsObservable) => {
    return action$.ofType(SessionActions.LOGIN_USER)
      .flatMap(({payload}) => http.post(`${BASE_URL}/auth/login`, payload))
      .map(result => ({
        type: SessionActions.LOGIN_USER_SUCCESS,
        payload: result.json().meta
      }))
      .catch(error => Observable.of({
        type: SessionActions.LOGIN_USER_ERROR
      }));
  }

This works great the first time the epic is triggered (by dispatching the LOGIN_USER action). If the command succeeds, you can even dispatch it several times.

However in the failure case, once the error is caught, the action$ stream is finished and I can’t retry because this epic no longer appears to be listening.

Do you have any advice? What I think I need is a way to handle an error in the stream and then continue the subscription to the original stream.

@matthewwithanm btw, if you want to still emit something that looks fairly close to how a real uncaught error looks like (stack trace and all that)

In Chrome, this works:

console.error('Uncaught', error.stack);

image

Updated my above comment to reflect.

@SethDavenport That’s incredibly great to hear. I tweeted something related today

You can implement redux with RxJS instead, but is there a strong reason? They have strong community, dev tools, lots of middleware, etc.

https://twitter.com/_jayphelps/status/754092387144638464

Thanks - yes http.post does return an observable (this is in the context of an angular2 project).

Turns out this works too:

  loginEpic = (action$: ActionsObservable) => {
    return action$.ofType(SessionActions.LOGIN_USER)
      .flatMap(({payload}) => http.post(`${BASE_URL}/auth/login`, payload))
      .map(result => ({
        type: SessionActions.LOGIN_USER_SUCCESS,
        payload: result.json().meta
      }))
      .retry(error => Observable.of({
        type: SessionActions.LOGIN_USER_ERROR
      }));
  }

(using retry instead of catch).