ngconf
Published in

ngconf

Reactive error-handling in Angular

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

error-handling with RxJs

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:

unhappy user
  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

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>

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 */);
}

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;
},
});
}
}

The notification service is straightforward:

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

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);
}

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 */);
}

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 */);
}

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’;
/* use router instead, if you want to stay
in your Angular application */
}
}

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

@NgModule(
{ providers:
[{provide: ErrorHandler, useClass: GlobalErrorHandler}]
})

The whole picture — all errors together

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]

Ready to build the most reliable web applications possible? Join us for the Reliable Web Summit this August 26th-27th, 2021 to learn how! A summit conference on building reliable web applications, including the following three pillars:

  • Scalable infrastructure and architecture
  • Maintainable Code
  • Automated Testing

https://reliablewebsummit.com/

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store