Tap That Error

Handling Errors from `tap()` in Redux-Observable Epics with Async Side Effects

It's important to ensure you are correctly handling errors in your application, but it becomes all the more crucial in a large redux codebase with with many engineers coding coding hundreds if not thousands of redux-observable epics in parallel. The rxjs operator, tap() is a foot gun in this sense. It's intended to be used for side effects, but what you may not realize is that it does not allow for handling errors from async operations.

The Problem with tap() and Async Functions

tap() is a handy operator for side effects in observables, but it wasn’t designed to handle asynchronous operations directly. If you use an async function within tap(), and it throws an error (e.g. a rejected Promise), that error won’t propagate down the observable stream as you might expect. The result? catchError doesn’t see it, and the error goes unhandled.

So, how do we fix this? Let’s look at some solutions that work well in large applications with many epics.

safeTap()

One of the easiest ways to handle async errors from tap() is to replace it with a safeTap utility function. This function wraps the async operation and catches any errors, logging them or even re-emitting them as observable errors if needed.

function safeTap(asyncFn: (...args: any[]) => Promise<any>) {
  return tap({
    next: async (...args) => {
      try {
        await asyncFn(...args);
      } catch (error) {
        console.error('Global error handler:', error);
        // Optionally, dispatch an error action or handle as needed
      }
    }
  });
}

Now, by using safeTap, async errors from tap() can be handled.

    #rxjs
    #error-handling