RFC: default error handling of Epics

Author: jayphelpsCreated Aug 6, 2016Updated Dec 9, 2025

The middleware is intentionally designed to accept a single, root Epic. This means that all your Epics are typically composed together using combineEpics(...epics).

The problem with this approach is that if an exception is thrown inside one of your epics and it isn't caught by you, it will "bubble" up all the way to the root epic and cause the entire output stream to terminate aka error(). As an example, Observable.ajax() currently throws exceptions for non-200 responses. So one bad AJAX request can terminate all of your epics and your app no longer listens for actions to handle side effects.

You can demo this sort of behavior here: http://jsbin.com/yowiduh/edit?js,output notice that it works the first time, errors the second, and any subsiquent time does nothing because the epic has terminated!

This is not unlike most programming models (where an uncaught exception terminates the "process"). In fact, this behavior is the same as if you were doing the equivalent in imperative JS with generators or similar. So RxJS is "correct" in the implementation.

However, because this has critical implications, we've been discussing adding some default behavior to mitigate this; especially for people who may be fairly new to Rx and not realize the implications.

Here are a couple options:

  1. .catch() it and and resubscribe to the root epic, basically "restarting" epics to continue listening.
    • Epics that maintain internal state will have lost that state completely, even ones that didn't cause the exception.
  2. or have combineEpics() listen for individual epics throwing exceptions and restart only the one that produced the error.

Alternative suggestions are welcome.


If we proof-of-concept option 1. it might look something like this:

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

javascript
const output$ = rootEpic(action$, store).catch((error, source) => {
  Observable.throw(error, Scheduler.async).subscribe();
  return source;
});

output$.subscribe(store.dispatch);

I'm not sure we want to do Observable.throw(error, Scheduler.async).subscribe(); but I used it here because I do want the exception to be thrown so window.onerror sees it and it shows up in the console as expected, but we also need to return the original source observable so I'm using Scheduler.async to "defer" the exception. There's prolly a better way to do this. A simple setTimeout prolly is ok instead.

Cc/ @blesh

Source: redux-observable/redux-observable