Angular Best Practice: RxJS Error Handling

by | Feb 25, 2019

RxJS is a big part of Angular.  Without a good understanding of how to do error handling the right way with RxJS, you are sure to run into weird problems down the line when an error does occur.  In contrast, if you know what you are doing up front, you can eliminate those weird problems and save yourself some debugging pain.

This article will examine:

  • The type of RxJS Observables to be most concerned about
    • RxJS Infinite Observables (see on the difference between finite and infinite Observables – although you can probably guess)
  • How to incorrectly handle an error in RxJS
    • What happens when the error is incorrectly handled?
  • What happens when you don’t handle an error in RxJS?
  • How to correctly handle an error in RxJS

The code for this article is available on github.

 

Infinite Observables

This article will be dealing with infinite observables – those that you expect to keep getting values from.  That’s because if you do error handling wrong, they cease to be infinite observables and finish – which will be very bad since your application is expecting it to be infinite.

These will be studied:

  • DOM Event – A DOM Event ‘keyup’ that you want to debounce as the user types on the page and then look up using an API
  • NgRx Effect – An NgRx Effect that you expect will always be listening for dispatched actions

 

DOM Event Case Study

The first case study will focus on handling DOM events and doing searches based off of them.  There will be two input boxes that you type in a Star Wars character’s name.  When you stop typing for 300 milliseconds and the text is different than the last one, it will search for those names using the Star Wars API and display the results.  The first input box will continue to work after an error.  The second input box will stop working after an error.

Here is the interface:

I’ve gamed this a little bit so that if you type “error”, it will search with a wrong URL, thus creating an error.

Here is the relevant HTML:

<input class="form-control" (keyup)="searchTerm$.next($event.target.value)" />

<input class="form-control" (keyup)="searchTermError$.next($event.target.value)" />

The keyup event simply emits using the “next” method of the Subject.

This is the component code:

    searchTerm$ = new Subject<string>();
    searchTermError$ = new Subject<string>();

    this.rxjsService
      .searchBadCatch(this.searchTermError$)
      .pipe(
        finalize(() =>
          console.log("searchTermError$ (bad catch) finalize called!")
        )
      )
      .subscribe(results => {
        console.log("Got results from search (bad catch)");
        this.resultsError = results.results;
      });

    this.rxjsService
      .search(this.searchTerm$)
      .pipe(finalize(() => console.log("searchTerm$ finalize called!")))
      .subscribe(results => {
        console.log("Got results from search (good catch)");
        this.results = results.results;
      });

This code basically will be reporting results to the page and logging whether it was called or not.  Note that we are calling two different service methods and passing in the two different Subjects.

The error handling code for this case study is in the rxjsService:

  searchBadCatch(terms: Observable<string>) {
    return terms.pipe(
      debounceTime(300),
      distinctUntilChanged(),
      switchMap(term => this.searchStarWarsNames(term)),
      catchError(error => {
        console.log("Caught search error the wrong way!");
        return of({ results: null });
      })
    );
  }

  search(terms: Observable<string>) {
    return terms.pipe(
      debounceTime(300),
      distinctUntilChanged(),
      switchMap(term =>
        this.searchStarWarsNames(term).pipe(
          catchError(error => {
            console.log("Caught search error the right way!");
            return of({ results: null });
          })
        )
      )
    );
  }

  private searchStarWarsNames(term) {
    let url = `https://swapi.co/api/people/?search=${term}`;
    if (term === "error") {
      url = `https://swapi.co/apix/people/?search=${term}`;
    }

    return this.http.get<any>(url);
  }

 

 

Bad Error Handling

The “searchBadCatch” method has our bad error handling code.  Just looking at it, it looks fine, right?  It is debouncing for 300 ms, has the distinctUntilChanged to make sure we don’t search for the same thing twice in a row.  There is a switchMap that calls the “searchStarWarsNames” method and we are catching errors using the catchError method.  What’s wrong with it?

If you catch the error using “catchError” at the first level of the Observables “pipe” method (in this case return terms.pipe()), it will allow you to handle the error and return one more result in the stream but will then end the observable stream.  And that means it won’t listen to “keyup” events anymore.  Therefore, at all cost, never allow an error to percolate to this level.

Note that if “catchError” is reached on the first level of the Observable “pipe” method, the “finalize” operator will be called.  You can see that up in the component code.

Here is a visual I hope will help:

Never let an error percolate to the level of the red line.

 

Good Error Handling

The “search” method has our RxJS best practice error handling code:

Always put the “catchError” operator inside a switchMap (or similar) so that it only ends the API call stream and then returns the stream to the switchMap, which continues the Observable.

If you are not calling an API, make sure to add a try/catch block so that you can handle the error in the catch block and not allow it to percolate to the first level “pipe”.  Don’t assume your code will “never fail”, use a try/catch block.

So you can see in the code that we add a “pipe” to the “searchStarWarsNames” call so that inside of there, we catchError and thus not allow the error to percolate to the first level “pipe”.

And the best practice visual:

Always catch errors inside of the switchMap/mergeMap/concatMap, etc.

 

Output

Now it’s time to see how this works on the website.  We can assume that it works at first.  The fun begins when an error is generated from the API call.

First, I’ll type “error” in both input boxes as shown:

I’ll leave it as an exercise for you to see the console output.  Now for the real test, can I type in something and get results after handling the error?

Here we see that the first one works and the second one doesn’t anymore:

The second input box is what I was talking about with a “weird problem” in the introduction.  You would have a tough time figuring out why your search quit working.

 

NgRx Effect Case Study

The reason I started writing this article was that I had a “weird problem” with one of my applications that was using NgRx and Effects.  See for information on effects.  Could it be that I wasn’t properly handling RxJS errors inside the effect?  As you’ll see in this study, the answer is “yes”.

Here is the interface:

Nothing fancy here:

  • “Success” – calls the Star Wars API with “person/1” (Luke Skywalker) and outputs the name on the screen
  • “Error – Stops Listening” – calls the API with a wrong URL so it generates an error – the catch is done wrong so it stops listening for the effect
  • “Error – Don’t catch error” – calls the API with a wrong URL so it generates an error – not catching the error
  • “Error – Keeps Listening” – calls the API with a wrong URL so it generates an error – properly catching the error so you can click it multiple times

I’ll skip the HTML for this since it’s just buttons calling component methods.  Here is the component code:

  ngrxSuccess() {
    this.store.dispatch(new CallWithoutError());
  }

  ngrxError() {
    this.store.dispatch(new CallWithError());
  }

  ngrxErrorKeepListening() {
    this.store.dispatch(new CallWithErrorKeepListening());
  }

  ngrxErrorDontCatch() {
    this.store.dispatch(new CallWithErrorNotCaught());
  }

The error handling good, bad and ugly is in the effect code.

 

 

CallWithoutError Effect

This is our success case:

  @Effect()
  callWithoutError$ = this.actions$.pipe(
    ofType(AppActionTypes.CallWithoutError),
    switchMap(() => {
      console.log("Calling api without error");

      return this.http.get<any>(`https://swapi.co/api/people/1`).pipe(
        map(results => results.name),
        switchMap(name => of(new SetName(name))),
        catchError(error => of(new SetName("Error!")))
      );
    }),
    finalize(() => console.log("CallWithoutError finalize called!"))
  );

This one will work every time but if it failed, it would continue working because the “catchError” is inside the http.get “pipe”.  For the success case, the SetName reducer will add the “name” to the store.  The UI picks that up and displays it.

 

CallWithError Effect

This effect will call the API with the wrong URL so an error is generated.  The error handling is done incorrectly so once called, this will never work again until the app is refreshed.

  @Effect()
  callWithError$ = this.actions$.pipe(
    ofType(AppActionTypes.CallWithError),
    switchMap(() => {
      console.log("Calling api with error - stop listening");

      return this.http.get<any>(`https://swapi.co/apix/people/1`).pipe(
        map(results => results.name),
        switchMap(name => of(new SetName(name)))
      );
    }),
    catchError(error => of(new SetName("Error - You're doomed!"))),
    finalize(() => console.log("CallWithError finalize called!"))
  );

In this case, the “catchError” at the first level of the this.actions$.pipe will get called, thus ending the effect because its Observable stream will end.  This is just like in the case study above using just RxJS Observables.  We should see “Error – You’re doomed!” on the page after clicking it.  If we try clicking that button again, it will not fire the effect.

Here is the output for this:

 

 

CallWithErrorKeepListening Effect

This effect will call the API with the wrong URL so an error is generated.  However, it will handle the error properly so that it can be called again.

  @Effect()
  callWithErrorKeepListening$ = this.actions$.pipe(
    ofType(AppActionTypes.CallWithErrorKeepListening),
    switchMap(() => {
      console.log("Calling api with error - keep listening");

      return this.http.get<any>(`https://swapi.co/apix/people/1`).pipe(
        map(results => results.name),
        switchMap(name => of(new SetName(name))),
        catchError(error => of(new SetName("Error but still listening!")))
      );
    }),
    finalize(() => console.log("CallWithErrorKeepListening finalize called!"))
  );

The right way to handle the RxJS error is by putting the “catchError” inside the http.get “pipe”.  It will end the http.get observable but that doesn’t matter because it is a finite observable anyways and only emits one value.  When it returns the SetName action, the switchMap will emit it and continue the Observable stream.  Note that the finalize here will never be called.

Here is the output for this:

 

 

CallWithErrorNotCaught Effect

This is our last effect and answers our question of “What happens if we don’t catch the error?”  The answer to that question is that it behaves the same way as if we handled the error improperly (since that’s how RxJS rolls).  It’s just that you aren’t hooking into that error stream.

  @Effect()
  callWithErrorDontCatch$ = this.actions$.pipe(
    ofType(AppActionTypes.CallWithErrorNotCaught),
    switchMap(() => {
      console.log("Calling api with error - don't catch");

      return this.http.get<any>(`https://swapi.co/apix/people/1`).pipe(
        map(results => results.name),
        switchMap(name => of(new SetName(name)))
      );
    }),
    finalize(() => console.log("CallWithErrorNotCaught finalize called!"))
  );

Also, the name on the UI will not be set since you are not calling SetName in the “catchError” operator.  So you will either not see any output if it was the first button clicked or you’ll see the last name that was set.  Another “weird problem” that would be hard to debug.

 

 

Conclusion

As you can tell from this article, knowing how to properly handle RxJS errors in your Angular application will help you to prevent those “weird problems” you could see when your infinite Observable stream ends unexpectedly.  Using this knowledge, you should be able to ensure that your infinite Observables never end until you decide they are finished.