How to prevent Observable error propagation?

Issue

I have a service that I use to auto-fetch data at regular intervals:

export class ApiService {
    interval$: BehaviorSubject<number> = new BehaviorSubject<number>(0); // Feed a 0 initially so it makes HTTP call even if auto-refresh is 0
                                                                         // variable ending in $ is common convention to mean it is an Observable
    constructor(private http: HttpClient) { }

    getApi(url: string, auto_refresh=false) {
        if (!auto_refresh)
            return this.http.get(url);

        return this.interval$.pipe(
            switchMap(duration => {
                if (duration == 0)
                    return this.http.get(url);

                return interval(duration * 1000).pipe(
                    startWith(0),
                    switchMap(() => this.http.get(url))
                )
            })
        );
    }

    updateInterval(i: number) {
        this.interval$.next(i);
    }
}

This works great if I do something in a component like:

this.subscription = this.apiService.getApi('/api/foo/bar', true).subscribe(tempjson => {
    this.foo = tempjson;
});

If I have auto-refresh interval set to 1, it will fetch /api/foo/bar every second.

The problem is if the API returns a non 200 return code. In this case, it seems to break the Observable and never tries doing a GET ever again.

I can’t figure out the root cause for this. I am guessing some sort of exception is propagating out of the Observable and makes the Observable get destroyed. But I can’t figure out how to prevent it. I tried adding an error handler to the subscription, but that doesn’t make any difference:

this.subscriptions.push(this.apiService.getApi('/api/modem/lte_signal_info', true).subscribe(tempjson => {
  this.lte_signal_info = tempjson;
},
error => {
  console.log(error)
}));

I also tried catching the error in the service itself, but it seems like you can’t just swallow the exception, you have to rethrow it when you are done as per: https://angular.io/guide/http#getting-error-details

Solution

As per the Observable design, if an error (exception) occurs in the observable pipeline then observable is in error state and it cannot emit new values (https://blog.angular-university.io/rxjs-error-handling/) and it can be considered as completed [i.e it cannot emit new values]. Because of this reason, if API returns a non 200 code, your observable is in error state and it will not emit new values.

Now to keep the source observable live in case of error (in your case interval observable keep running in case of error), handle the error in the observable which throws an error by using catchError operator. Change your code like this:

getApi(url: string, auto_refresh=false) {
      if (!auto_refresh)
          return this.http.get(url);

      return this.interval$.pipe(
          switchMap(duration => {
              if (duration == 0)
                  return this.http.get(url)
                             .pipe(
                               catchError(err => {

                                 //return an observable as per your logic
                                 //for now I am returning error wrapped in an observable
                                 //as per your logic you may process the error
                                 return of(err);
                               })
                             );

              return interval(duration * 1000).pipe(
                  startWith(0),
                  switchMap(() => {
                    return this.http.get(url)
                             .pipe(
                               catchError(err => {
                                 //return an observable as per your logic
                                 //for now I am returning error wrapped in an observable
                                 //as per your logic you may process the error
                                 return of(err);
                               })
                             );
                  })
              )
          })
      );

Of course, you can write a function to put the duplicate code [as you can see in the above code] and use that method.

Hope it will give you an idea and solves your problem.

Answered By – user2216584

Answer Checked By – Katrina (AngularFixing Volunteer)

Leave a Reply

Your email address will not be published.