Tap That Error

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

The RxJS tap() operator is a footgun. It's meant for side effects, but if you pass it an async function that throws, that error vanishes. No catchError, no warning—just silence.

Why This Happens

tap() is handy in observables, but it wasn’t designed to handle asynchronous operations directly. If your callback returns a Promise that rejects, RxJS doesn't wait for it or catch it and as a result the error won’t propagate down the observable stream as you might expect. The result? Your downstream catchError doesn’t see it and the error goes unhandled.

The Fix: safeTap()

Wrap async operations in a utility that catches errors explicitly:

function safeTap<T>(asyncFn: (value: T) => Promise<void>) {
  return tap({
    next: async (value: T) => {
      try {
        await asyncFn(value);
      } catch (error) {
        console.error('safeTap caught:', error);
        // Optionally dispatch an error action here
      }
    }
  });
}

Now use safeTap() instead of tap() for async side effects, and errors actually get handled.

  • rxjs
  • error-handling