Search code examples
rxjsngrxngrx-effectsrxjs-observables

Keeping error information and the outer observable alive


To ensure an error doesn't complete the outer observable, a common rxjs effects pattern I've adopted is:

 public saySomething$: Observable<Action> = createEffect(() => {

    return this.actions.pipe(

      ofType<AppActions.SaySomething>(AppActions.SAY_SOMETHING),

      // Switch to the result of the inner observable.
      switchMap((action) => {
        // This service could fail.
        return this.service.saySomething(action.payload).pipe(
          // Return `null` to keep the outer observable alive!
          catchError((error) => {
            // What can I do with error here?
            return of(null);
          })
        )
      }),

      // The result could be null because something could go wrong.
      tap((result: Result | null) => {
        if (result) {
          // Do something with the result!
        }
      }),

      // Update the store state.
      map((result: Result | null) => {
        if (result) {
          return new AppActions.SaySomethingSuccess(result);
        }
        // It would be nice if I had access the **error** here. 
        return new AppActions.SaySomethingFail();
      }));
});

Notice that I'm using catchError on the inner observable to keep the outer observable alive if the underlying network call fails (service.saySomething(action.payload)):

catchError((error) => {
  // What can I do with error here?
  return of(null);
})

The subsequent tap and map operators accommodate this in their signatures by allowing null, i.e. (result: Result | null). However, I lose the error information. Ultimately when the final map method returns new AppActions.SaySomethingFail(); I have lost any information about the error.

How can I keep the error information throughout the pipe rather than losing it at the point it's caught?


Solution

  • As suggested in comments you should use Type guard function

    Unfortunately I can't run typescript in snippet so I commented types

    const { of, throwError, operators: {
        switchMap,
        tap,
        map,
        catchError
      }
    } = rxjs;
    
    const actions = of({payload: 'data'});
    
    const service = {
      saySomething: () => throwError(new Error('test'))
    }
    
    const AppActions = {
    }
    
    AppActions.SaySomethingSuccess = function () {
    }
    AppActions.SaySomethingFail = function() {
    }
    
    /* Type guard */
    function isError(value/*: Result | Error*/)/* value is Error*/ {
      return value instanceof Error;
    }
    
    const observable = actions.pipe(
      switchMap((action) => {
        
        return service.saySomething(action.payload).pipe(
          catchError((error) => {
            return of(error);
          })
        )
      }),
      tap((result/*: Result | Error*/) => {
        if (isError(result)) {
          console.log('tap error')
          return;
        }
        
        console.log('tap result');
      }),
      map((result/*: Result | Error*/) => {
        if (isError(result)) {
          console.log('map error')
          return new AppActions.SaySomethingFail();
        }
        
        console.log('map result');
        return new AppActions.SaySomethingSuccess(result);
      }));
      
      observable.subscribe(_ => {
    
      })
    <script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.5.5/rxjs.umd.js"></script>