Friday, March 29, 2024

RxJS Error Handling Using catchError()

Error handling plays an essential part in just about any application, doubly so when dealing with network calls. For that reason, libraries like RxJS include a wide variety of mechanisms for dealing with errors of every conceivable sort in a graceful way. In fact, RxJS lends itself to so many error handling strategies that developers are often unsure which to use for a given situation. This web development tutorial aims to clear up the confusion by providing a few concrete examples that you can follow in your own coding.

Success, Error, Complete ≠ Try, Catch, Finally

On initial observation, the three RxJS subscribe() function parameters bear a close resemblance to the try, catch, finally blocks of traditional JavaScript (JS) error handling. In fact, they are similar in several ways:

  • In both cases, the complete() function and finally block are optional.
  • The error() function and catch block both allow us to handle errors in a graceful manner.
  • Both the error() function and catch block give us the option of handling the error ourselves.

Beyond these similarities, the success, error, and complete functions of RxJS differs from its synchronous JS counterpart in one very important respect:

Whereas the finally block executes no matter what happens before it, the complete() function is only invoked if the stream completes, i.e, if the stream does not error out.

Read: Graceful RxJS Error Handling

We can verify that behavior easily enough by fetching both a valid and invalid URL using the HttpClient’s get method:

import { HttpClient } from '@angular/common/http';
import { Component } from '@angular/core';

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: [ './app.component.css' ],
  providers: [HttpClient]
})
export class AppComponent  {

  constructor(private http: HttpClient) {}

  ngOnInit() {
    const http$ = 
      this.http.get(
        'mattree-node-highlighting-demo?file=main.ts', 
        {responseType: 'text'}
      ); 

    http$.subscribe({
      next: (res) => console.log(res),
      error: (err) => console.error(err),
      complete: () => console.info('complete') 
    });
  }
}

We can see that both the HTML content and ‘complete’ message were printed to the console:

RxJS Error Handling Example

 

Changing the URL parameter to something that fails now only outputs the error:

Error Handling in RxJS

 

Now imagine that you wanted to show a dialog after a POST operation that updates user permissions. You might like to pass along an error message as follows:

constructor(public dialogRef: MatDialog) { }

const dialogConfig = new MatDialogConfig();
this.appService.updatePermissions(this.permissionId, this.permissions)
  .subscribe(
    () => '',
    (err) => dialogConfig.data = { err: err ? err : "Something went wrong! Couldn't save permissions." },
    () => this.dialogRef.open(DialogComponent, dialogConfig)
  );

Knowing what we do now about how subscribe handlers work, it is obvious that the dialog won’t appear when an error is encountered.

Read: RxJS Observables Primer in Angular

Continuing On Error in RxJS Error Handling

To make the above code work as intended, we need to catch the error before the subscribe and send the error state along for handling there. For that purpose, there is the carchError() operator. It can return a new observable (or error) so we can log the error for debugging purposes and then return a Boolean indicating that an error was encountered.

The subscribe() will now receive the error observable so that it can let the dialog know whether or not the save operation was successful:

interface SaveDialogData {
  success?: boolean;
}

this.appService.updatePermissions(this.permissionId, this.permissions)
  .pipe(
    catchError(err => {
      if (err) {
        console.error(err);
      }
      return of(err != null);
    })
  )
  .subscribe((err: boolean) => {
    const dialogConfig = new MatDialogConfig<SaveDialogData>();
    if (err) {
      dialogConfig.data = { success: false };
    }
    this.dialogRef.open(NewsDialogComponent, dialogConfig);
  });

This particular type of error treatment is known as the “Catch and Replace Strategy”, whereby catchError() is employed to provide a replacement Observable that emits a fallback value or values. The trick to making this strategy work is to pass whatever replacement value(s) we want to send along to the of() method. It can accept a variable amount of values in a sequence and then immediately emits a complete notification. It is similar to from(), but does not do any flattening and emits each argument in whole as one next notification.

Here is its signature:

of<T>(...args): Observable<T>

So, even though we are only sending a single Boolean value, we could just as easily have passed an array of values as follows:

return of(err, this.permissionId, this.permissions);

Read: Executing RxJS 6 Observables in Order

Accessing the success Boolean in the Dialog Component

In the DialogComponent class, we can inject the data via the constructor and set public variable accordingly:

export class DialogComponent implements OnInit {
  public success: boolean = true;

  constructor(
    @Inject(MAT_DIALOG_DATA) public data: SaveDialogData
  ) { }

  ngOnInit(): void {
    if (this.data?.success === false) {
      this.success = false;
    }
  }
}

This technique provides an added benefit that the dialog message can be set in the template, making it much easier to translate if necessary:

<h2 mat-dialog-title>Save Permissions</h2>
<div mat-dialog-content>
  {{success ? "Permissions have been successfully saved" : "Something went wrong! Couldn't save permissions."}}
</div>
<div mat-dialog-actions>
    <button mat-button mat-dialog-close>Close</button>
</div> 

The catchError() RxJS Demo

To better understand the catchError() operator, here is a fully editable demo that shows what happens when an API call returns successfully and errors out. This is achieved via two buttons.

The Send valid request emulates a successful Save operation:

RxJS error handling

 

The Send invalid request emulates a failed Save operation:

RxJS How to Handle Errors

 

Conclusion to RxJS Error Handling Using catchError()

While similar the finally block of traditional JavaScript (JS) error handling, the RxJS subscribe() method’s complete() function parameter is not invoked when an asynchronous call errors out. For that reason, the catchError() operator is your best bet for handling errors when you want to continue on whether an error occurs or not.

Read more RxJS and web development tutorials.

Rob Gravelle
Rob Gravelle
Rob Gravelle resides in Ottawa, Canada, and has been an IT guru for over 20 years. In that time, Rob has built systems for intelligence-related organizations such as Canada Border Services and various commercial businesses. In his spare time, Rob has become an accomplished music artist with several CDs and digital releases to his credit.

Get the Free Newsletter!

Subscribe to Developer Insider for top news, trends & analysis

Popular Articles

Featured