DEV Community

loading...
Cover image for Reactive error-handling in Angular

Reactive error-handling in Angular

ngconf profile image ng-conf ・7 min read

Maria Korneeva | ng-conf | Nov 2020

“Whatever can go wrong, will go wrong.” © Murphy’s Law

A black and white drawn image, the design is sketchy in look. The image consists of multiple triangle signs with "!" inside, two circle and hexagonal signs with "X" inside, a circle sign with "-" inside, and an Angular logo all around a large triangle sign. The large triangle sign has a drawing of an eel looking creature curled in a circle.

Error-handling is an architectural decision, and like any other architectural decision, it depends on the project goals and setup. In this article, I’m going to describe one of the possible ways to handle errors in your app(s) that proved useful for an enterprise portal.

Before we move on to the implementation, let’s have a look at some trade-offs of error-handling:

  • User: you want to be as user-friendly as possible: “Dear user, a tiny error has occurred. But please do not worry! I am here for you to protect you from the danger and to find the best solution. Trust me, I have a plan B”.
  • Security: you don’t want to leak any implementation details, any unconventional return codes, any hints to regex etc.
  • You want to track your errors to improve UX, to increase conversion rate, to reduce hot-fixes, to make the world better.

The overall idea is to differentiate between 4 types of errors:

A black and white image in the same sketchy style. The image is of a simple stick figure girl with short hair, arms crossed and an upset face. Behind the girl is a browser window with the classic "X's for eyes" dead face.

  1. known (validation) errors: the user has the chance to fix it by re-entering correct data,
  2. known errors: the expected data cannot be loaded / updated,
  3. known errors: the user wouldn’t notice the error,
  4. unknown errors: yes, they do exist!

The rule of thumb for handling these errors is:

  1. Be as specific as possible. Let the user know what and how to correct.
  2. Explain what was not successful.
  3. Poker face (don’t show any error message)
  4. Fall-back scenario (e.g. redirect to an error page)

Let’s have a look at each of them.

Validation errors

Black and white image in the same sketched style. The image has two long, thin rectangles stacked over each other with a chat bubble in between. The rectangle on top has a small frowny face on the right side. The chat bubble reads "You are lying to me" in all caps, and the bottom rectangle has a circle with a check mark on the right side.

As with any error, prevention is the best error-handling. So before reading this article, make sure, you’ve taken enough care of frontend validation including formatting, parsing, regex, cross-field checks, and further stuff, before sending your data to the server.

As with any error, validation errors can still happen. The good news is, however, that the user has a chance to fix it by altering his/her input. That is why you have to be as specific as possible (and as allowed by the security policy — no need to expose too much of internal implementation or to help with the password/username-fields).

So, in your component template:

<form>
   <input [class.error]=”isValidationError”/>
   <p class="errorText" *ngIf=isValidationError>
      {{validationError}}
   </p>
   <button (click)="submitForm()">Submit</button>
</form>
Enter fullscreen mode Exit fullscreen mode

In your component.ts:

public submitForm()
{
   this.service.sendForm()
   .pipe(catchError((e: HttpErrorResponse)=>{
      if (e.status === 422){
         this.showValidationError = true;
         this.validationError = e.error.error;
         return of(null);
      }
   }))
   // TODO: Catch other errors: cf. next section
   .subscribe(//TODO: Handle success);
}
Enter fullscreen mode Exit fullscreen mode

The logic is pretty simple: as soon as a validation error occurs, display the respective message and update the UI (e.g. red border of the input field). We assume here that a validation error means http return code 422 + validation message from your server.

Please note that this is just a rudimentary error-handling example to illustrate the main idea. For further guidance, I’d recommend reading the article “How to Report Errors in Forms: 10 Design Guidelines”.

Note the TODO in the code — you still have to deal with other types of errors. This will be handled in the next section.

Known errors that have to be addressed in UI

If you are trying to load the list of heroes or personal data or whatever stuff you need to display to the user, you have to be prepared for the worst case. In this section, we are talking about errors that have to be explained/displayed in the UI. In my experience, this is the most frequent scenario. There is no particular input field that the error belongs to. That is why in this case a dedicated error-component and a reactive notification service make sense.

This is what it could look like:

@Component({
   selector: ‘error-component’,
   template: `<p *ngIf="errorMessage">{{errorMessage}}</p>`,
   styles: [`p { color: red }`]
})
export class ErrorComponent {
   public errorMessage = ‘’;
   constructor(private errorNotificationService:
                                ErrorNotificationService){}
   public ngOnInit() {
      this.errorNotificationService.notification.subscribe({
         next: (notification) => {
               this.errorMessage = notification;
         },
      });
   }
}
Enter fullscreen mode Exit fullscreen mode

The notification service is straightforward:

@Injectable()
export class ErrorNotificationService {
   public notification = new BehaviorSubject<string | null>(null);
}
Enter fullscreen mode Exit fullscreen mode

The error-handling flow would be: whenever (and wherever) an error occurs, call notification.next() and pass the error-specific message: this.errorNotificationService.notification.next('Some error message') Error-component subscribes to the changes and displays the corresponding text. Hence, error-component should be placed on each page (e.g. as part of the header-component). Note that this approach allows you to use custom error messages for each service. If this is not necessary, check an alternative solution based on http-interceptors.

Since we are talking about reactive error-handling and for the sake of further DRY-ness, we could refactor our code. Let’s introduce ErrorHandlingService that takes care of calling the ErrorNotificationService. Note, that we’ve added KNOWN_ERRORS. With this option, you can decide, which errors should be handled by your component and which ones should be passed to the global ErrorHandler — e.g. 500 or 503 (more on this in the section “Global error-handling”).

const KNOWN_ERRORS = [400, 401, 403];
@Injectable()
   export class ErrorHandlingService {
constructor(private errorNotificationService: 
                       ErrorNotificationService) {}
public handleError(errorMessage: string): 
        (errorResponse: HttpErrorResponse) => Observable<null> 
   {
       return (errorResponse: HttpErrorResponse) => 
       {
          if (isKnownError(errorResponse.status)) 
          {
             this.errorNotificationService
                         .notification.next(errorMessage);
              return of(null); 
          }
          throwError(errorResponse)};
       }
   }
}
/*** @description it returns true for all errors, 
* known in the app, so that no redirect to error-page takes place
* @param errorCode — error status code
*/
export function isKnownError(errorCode: number): boolean {
   return KNOWN_ERRORS.includes(errorCode);
}
Enter fullscreen mode Exit fullscreen mode

With this, you can handle your errors just like this:

public doSomething()
{
   this.service.sendRequest()
   .pipe(
       catchError(
          this.errorHandlingService
                  .handleError(‘An error occurred in sendRequest’)))
   .subscribe(//TODO: handle success);
}
Enter fullscreen mode Exit fullscreen mode

If you have just one app, you can (and probably should) merge ErrorHandlingService and ErrorNotificationService for the sake of simplicity. In our case, we had to split it due to slight differences in the error-handling approaches.

Known errors without UI-display (a.k.a. silent errors)

When you load some additional stuff that is not strictly necessary for the main functionality, you don’t want to confuse the user with the error-message — e.g. if the loading of a commercial / teaser / banner failed. The handling here is pretty simple:

public loadBanner(){
   this.service.loadBanner()
    .pipe(catchError(()=>{return of(null)}))
    .subscribe(// TODO: handle success);
}
Enter fullscreen mode Exit fullscreen mode

By now we’ve handled all http-errors: either as a validation error or as a general error or as a silent error. However, things still can go wrong (e.g. promises! What about promises?!) That is why we need a further fall-back option — the global ErrorHandler.

Global error-handling

Luckily, Angular has already provided a global ErrorHandler for us. The default implementation of ErrorHandler prints error messages to the console. To intercept error handling, you need to write a custom exception handler that replaces this default as appropriate for your app.

Why should you replace the default ErrorHandler?

  • You should not user console.log in production. The reasons for this are well explained in the article “Deactivate console.log on production (Why and How)”.
  • You might want to add additional tracking for your global errors so that you can learn from it.
  • You might want to define a general behavior for all unhandled-errors, e.g. redirect to an error page.

The skeleton of such a global service could like like this:

@Injectable()
export class GlobalErrorHandler extends ErrorHandler {
public handleError(e: string | Error 
                      | HttpErrorResponse | unknown) {
      window.location.href = ‘/error-page’;
   }
}
Enter fullscreen mode Exit fullscreen mode

Don’t forget to add it to your app.module:

@NgModule(
 { providers: 
    [{provide: ErrorHandler, useClass: GlobalErrorHandler}] 
})
Enter fullscreen mode Exit fullscreen mode

The whole picture — all errors together

black and white image in a sketched style. At the top of the image is a folder icon, over top of that is a cloud labeled "TS". The file merges into 3 arrows underneath the TS cloud. Along with the arrows are small rain drops falling. Two of the three arrows are stopped on the right and left sides of the image by two strainers. The left is labeled "Validation Errors", the right "Silent Errors". The third arrow extends further down between the two strainers into a larger strainer labeled "General Errors". This strainer is placed underneath the majority of the two strainers above. Under all 3 is the larges strainer that stretches across the entire picture. This one is labeled "Global". The diagram shows fewer and fewer rain drops falling as they're collected by each layer of strainers. Under the last strainer is a stick figure girl with short hair. She is smiling and standing in the rays of a sun on the right corner. To the left are three birds flying.

The approach that I’ve described in this story resembles a set of sieves. Whatever gets through the upper level, gets caught by the next one, until the last ultimate (global) layer of error-handling.

I’ve illustrated the basics of this approach in a demo-app: https://angular-ivy-hsbvcu.stackblitz.io/error-demo


[Disclaimer: did I miss something / is something not quite correct? Please let me and other readers know AND provide missing/relevant/correct information in your comments — help other readers (and the author) to get it straight! a.k.a. #learningbysharing]

ng-conf: The Musical is coming

ng-conf: The Musical is a two-day conference from the ng-conf folks coming on April 22nd & 23rd, 2021. Check it out at ng-conf.org

Discussion (0)

Forem Open with the Forem app