3

Problem

Suppose there is a Http request observable that errored, we can just retry it. But I also want the UI to inform the user that this resource failed to load. What is the best architecture?

Intended Behavior for the Target Observable

  1. Retry-able.
  2. Long-running. Doesn't complete or error.
  3. Shared. Does not generate unnecessary requests when multiple subscriber.
  4. Load on need. Does not generate unnecessary requests when not subscribed.
  5. Inform UI of the errors.

(3 and 4 can be achieved by shareReplay({bufferSize: 1, refCount: true}))

My Attempts

I think it's best to pass an error message to the downstream observer while keeping retrying the source. It causes minimum changes to the architecture. But I didn't see a way I can do it with Rxjs, because

  1. retry() always intercepts the error. If you materialze the error, then retry() won't retry. If not, then no error will propagate to the downstream.
  2. catchError() without rethrowing will always complete the stream.

Although let the UI observer tap(,,onError) and retry() can satisfy this need, but I think it is dangerous to let the UI take this responsibility. And multiple UI observer means a LOT of duplicated retries.

First_Strike
  • 1,029
  • 1
  • 10
  • 27

2 Answers2

3

Well, I seem to have accidentally find the answer while browsing through the documentations.

It starts with the usage of the second parameter of the catchError. According to the documentation, retry is implemented by catchError. And we can express more logic with the lower-level catchError.

So it's just

catchError((err, caught) => {
  return timer(RETRY_DELAY_TIME).pipe(
    mergeMap(() => caught)
    startWith(err)
  );
})

It retries the observable, meanwhile sending error messages to the downstream observers. So the downstream is aware of the connection error, and can expect to receive retried values.

First_Strike
  • 1,029
  • 1
  • 10
  • 27
  • This code is going to wait the whole RETRY_DELAY_TIME before issuing the error to the subscriber. In my case that was 20 seconds so there's a long delay before the user gets any notification that something went wrong and we intend to retry. Here's an alternative that worked for me (with huge thanks to First_Strike for showing me how to do this in the first place): `catchError((err, caught) => { return concat( of(err), timer(RETRY_DELAY_TIME).pipe(mergeMap(() => caught)) ); })` – John Munsch Jun 04 '20 at 16:41
1

It sounds like you're looking for something akin to an NgRx side effect. You can encase it all in an outer Observable, piping the error handler to the inner Observable (your HTTP call), something like this:

const myObs$ = fromEvent('place event that triggers call here').pipe(
               // just one example, you can trigger this as you please
  switchMap(() => this.myHttpService.getResource().pipe(
    catchError(err => handleAndRethrowError()),
    retry(3)
  ),
  shareReplay()
);

This way, if the request throws an error, it is retried 3 times (with error handling in the catchError block, and even if it fully errors out, the outer Observable is still alive. Does that look like it makes sense?

Will Alexander
  • 3,425
  • 1
  • 10
  • 17
  • If I want to seperate the UI and the data infrasturcture, I can send the error message captured inside your `handleAndRethrowError()` to a `Subject` and let the UI subscribe to the `Subject`. Is this your intended solution? – First_Strike Sep 08 '19 at 18:17
  • That's one way of doing things, yes. Basically you can do any error handling you like in that `catchError` block. – Will Alexander Sep 08 '19 at 18:19
  • Thank you. If there is a better architecture for this (I'm still feeling my solution clumbersome), please let me know. – First_Strike Sep 08 '19 at 18:29