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.
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.
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.