1

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?

Jack
  • 10,313
  • 15
  • 75
  • 118
  • Can't you just `return of(error);`? – Józef Podlecki Jun 17 '20 at 17:04
  • @józef-podlecki I could, but then how would I distinguish in `tap` and `map` between an error and a successful result? – Jack Jun 17 '20 at 17:06
  • [...with a type guard?](https://www.typescriptlang.org/docs/handbook/advanced-types.html#type-guards-and-differentiating-types) – Józef Podlecki Jun 17 '20 at 17:08
  • That seems to cause an error `tap((result: Result | null) => result is Result {` is warning that `A function whose declared type is neither 'void' nor 'any' must return a value.ts(2355)`. – Jack Jun 17 '20 at 17:14

2 Answers2

0

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>
Józef Podlecki
  • 10,453
  • 5
  • 24
  • 50
  • Thanks for this. It looks like it'll do the job, but it doesn't seem very functional or ngrx-like. I'm surprised it comes down to inspecting the type of a variable considering I know at the point the error is caught, that I have an error and not a valid result. Another issue is that errors often have `any` type, which means I need to construct a new `Error` object so instance works reliably. This is starting to feel hacky. – Jack Jun 17 '20 at 17:55
  • I noticed that since the parameter to `tap` and `map` can now be either a `Result` or `Error` that the compiler complains that the value may not have certain (`Result`) properties, even after checking if it's an instance of `Error`. – Jack Jun 17 '20 at 18:10
  • how is `Result` defined? – Józef Podlecki Jun 17 '20 at 18:56
  • It's an interface; `interface Result { ... }`. My type guard wasn't returning a type predicate, so the compiler complained. After ensuring the return type is `value is Error` the compiler was happy. – Jack Jun 17 '20 at 20:08
  • [I tried on playground and intellisense didnt show any red flag](https://www.typescriptlang.org/play/#code/JYOwLgpgTgZghgYwgAgEoQM4FcA2ZkDeAUMsgA5QD2ZAjAFzIZhSgDmA3CeVWQEwMgsAWwBG0TqQrUAzA0oiAVhARhOAXyJEYWECuCUQyYBgCiUKlAAUANzg4sEBumx5kAH2RmLASga37KMae5pRQhFxQEGBYUIb+DkYgTHC6EJQwwRbqmtq6YPqGYHBkNnYOTpi4+B5eod7hpMAZlsa1VvEQ3vXEpKQIBhiUOBAAdDiUrKUBIyBwQp0Svf1JQ6Pjkx0j8xgYcKwLXKSR0bFcGlzLg8NjE1MOI1K03ouXqzcbZaOPvM8XA2-rO5fHjSX5qIA) – Józef Podlecki Jun 17 '20 at 20:14
  • Ah ok nvm. Didnt see the full response – Józef Podlecki Jun 17 '20 at 20:15
0

I wouldn't try to keep the error information throughout the pipe. Instead you should separate your success pipeline (tap, map) from your error pipeline (catchError) by adding all operators to the observable whose result they should actually work with, i.e. your inner observable.

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

    return this.actions.pipe(
      ofType<AppActions.SaySomething>(AppActions.SAY_SOMETHING),
      switchMap((action) => this.service.saySomething(action.payload).pipe(
        tap((result: Result) => {
          // Do something with the result!
        }),
        // Update the store state.
        map((result: Result) => {
          return new AppActions.SaySomethingSuccess(result);
        }),
        catchError((error) => {
          // I can access the **error** here. 
          return of(new AppActions.SaySomethingFail());
        })
      )),     
    );
});

This way tap and map will only be executed on success results from this.service.saySomething. Move all your error side effects and error mapping into catchError.

frido
  • 13,065
  • 5
  • 42
  • 56