Handling Errors in NgRx Effects

At CityPantry, we use NgRx for our state management, and on the whole it’s pretty nifty. Advantages include the fact that it’s really clear what the state looks like, and we can reason about what everything looks like. Reducers, since they are pure functions, are beautifully easy to test: “Given this state, and this input, do I get the right output? yes? great.”

One thing that took a while for us to get our heads around is NgRx Effects. It’s how you deal with asynchronous actions in Redux state; in short, it gives you a hook into your Redux store to say “every time an event is dispatched, after it’s been reduced, let me know.” What this means in a practical sense is that if we wanted to, say, fire off an analytics event whenever a page redirect happened, we could do the following:

The code here calls the AnalyticsService every time the ROUTER_NAVIGATION event is dispatched.

Asynchronous Calls with Return Values

The above example was a simple side effect, but most of the time we want to request data from the server and do something with it. In that case, you usually end up with two actions: FETCH_A_THING and THING_FETCHED . Your effect would look like this:

So here we’re doing the asynchronous action in a switchMap which will do the async call, and emit a new observable when our HTTP call completes. We then wrap the result of the HTTP call in a new action that gets dispatched automatically by the effects module. (Note that usually I would use an action creator to create the action inside the map, so it would just be map(todoListFetched) , but for the sake of clarity I created the action manually here.)

The Problem: Error Handling

How, now, do we handle errors? If we do nothing with them, we’ll never find out that an action failed, so the user might never get any feedback. So we should create an action that we can dispatch to update the store:

That should do the trick, right?

Except… now, once an error has been caught, we discover our effects stop working. If ever a FETCH_TODO_LIST_FAILED action is dispatched, we can no longer trigger the FETCH_TODO_LIST effects! But why?

It turns out the secret is in the catchError call. When catchError handles an error, it returns a new Observable (as is evident in the code). So far, so good — it looks a lot like a switchMap. But it doesn’t actually behave like one at all!

A switchMap takes an incoming Observable, creates a new (internal) Observable, and emits those internal Observable’s values whenever it emits. If a new value comes in, it stops the internal Observable, creates a new one from the incoming value, and starts emitting that new internal Observable’s values. It’s basically a wrapper around a set of Observables — and the important thing is it does not complete. If the internal Observable completes (e.g. because it’s an HTTP call and only ever emits one value), the switchMap stays active and just waits for the next incoming value to create a new internal Observable from.

On the other hand, catchError does not do any wrapping. It waits for the upstream Observable to fail, and if it does, it will create a new Observable and replace the upstream one with that one. Put simply, its internal logic is as follows: While the upstream Observable emits values, emit the same values; if the upstream Observable fails, start emitting values from the callback. The important part here is the realisation that once an Observable has failed, it will never go back to a successful state, so the catchError method lets you prevent that by replacing the failed one with a successful one. This is slightly unexpected but makes sense if you’re coming from a Promise-based world: In a promise, the .catch method lets you turn a failed promise back into a successful one. But because Promises are basically Observables of one value, this does not have any unexpected side effects, whereas in Observables saying “stop and emit this new value” will not allow future values to be emitted.

What does this mean for our effects? When we want to handle an error, it is not sufficient to put an error handler at the end of the chain. If we do that, the Observable that NgRx Effects subscribes to is going to be replaced by the Observable emitted in the catchError method, and then all future values emitted from action$ are going to be ignored (the new Observable in the error handler is not subscribed to them). This means we have to always place catchError handlers inside a switchMap — this way, the new (handled) Observable only replaces the failed one inside the switch, rather than the whole Effect Observable.

Aside: Marble Testing

The reason I spotted this, by the way, was not by clever introspection or knowing the source code inside out. I found it when I switched our unit tests for our effects to Marble Testing, which is the recommended way to test NgRx effects. What happened was that I was testing an error case, and got the rather cryptic message of Expected $[2] = Object({ frame: 20, notification: Notification({ kind: 'C', ... }) }) to equal undefined.

It turns out that what my test was telling me was that the Observable for the effect I was subscribed to had completed (kind: 'C' means Completed). This was entirely unexpected, as Effects Observables should never complete (if they complete, they cannot listen to and dispatch any further actions!) When we were testing without marbles, we had just tested if this value goes in, this value comes out, and never caught this since the value was correctly emitted. I would therefore highly recommend that if you don’t already do it, you should switch your Effects testing to the Marble tests.

Summary

It turns out that catchError (and its predecessor, .catch()), is a tricksy beast, making us think that it will just replace errors with new values, but actually switching entirely to a new Observable on the first error. It is not to be confused with switchMap’s effects which keep the Observable open, and should be used with care, as it will cause NgRx effects to stop working when simply used in a pipe on an effect. Use it inside switchMap to ensure that any caught errors do not complete the Effect Observable.

(If you have any other patterns for handling errors in Effects, please let me know, I would love to see other ways of dealing with this!)