Error Handling in Angular Applications
- the dismantling of the application into reusable components,
- services as an abstraction layer for business logic and/or data procurement,
- the modularization of components and services by means of angular modules,
- depedency injection and IoC Contrainer,
- command line tool “Angular CLI”, especially
live development using ng serve,
build via webpack, including treeshaking and minification,
run unit tests, - the creation of libraries for cross-application reuse,
- and Angular Elements for the conversion of Web components
There are certainly many positive things to report about Angular and various use cases. I would like to make a proposal to you today on error handling in Angular applications.
The requirements from logging to export
There are several ways to approach the issue of error handling on clients. Ultimately, these paths often work independently of the chosen technology. So it is secondary whether it is a native application like Word, Excel, Adobe Photoshop or a web application like Google Mail. In one of my projects I have identified the following essential requirements:
- All unexpected errors are logged, persisted, and can be retrieved in the client.
- Intercepting errors should be as simple and continuous as possible.
- The user should be made aware of these errors, either through notifications and/or message boxes.
- The error list must be exportable for evaluations.
The identification of possible error sources
In order to implement the above requirements, it is important to identify the areas in your application that could generate errors. This often includes subscribe() calls to services that send an http request, or general errors such as calls to undefined/null properties.
The Subscribe Error Handling
Subscriptions may contain such a code:
this.myService.getMyData()
.subscribe(data => this.handleData(data),
error => console.log(error));
This writes any error caused by a next() on the observable to the browser log. Basically sufficient, but the user does not notice anything in case of an error; at best he notices that the application reacts “strangely”.
The generally occurring error
Theoretically and practically, an error can occur at any point in an application. What to do Angular offers a nice solution for this: the definition of a so-called ErrorHandler. Whenever an error is not captured, this handler is called. All you have to do is implement a class that you hang in your Angular application above the corresponding module – usually it might be useful in the AppModule:
class GlobalErrorHandler implements ErrorHandler {
handleError(error: any) {
//hier den Fehler behandeln.
}
}
@NgModule({
providers: [{provide: ErrorHandler, useClass: GlobalErrorHandler}]
})
class AppModule {}
Using the ErrorService
For the requirement to solve a problem recurrently easy, the implementation of an own service offers a good solution. Ideally I would call it ErrorService. There you can persist the errors of the client. In my example, I decided to log the last 20 errors rolling and store them in LocalStorage. The LocalStorage is limited to a few megabytes, so you should pay attention to the consumption. My implementation looks like this:
export class ErrorEntry {
timeStamp: Date;
description: string;
stack: string;
}
@Injectable({ providedIn: 'root' })
export class ErrorService {
private errors: ErrorEntry[];
constructor() {
const errorData = localStorage.getItem('error_data');
if (errorData) {
this.errors = JSON.parse(errorData) as ErrorEntry[];
} else {
this.errors = [];
}
}
logError(error: any) {
const maxElements = 50;
console.error(error);
const entry = {
timeStamp: new Date(),
description: '',
stack: ''
};
if (typeof error === 'string') {
entry.description = error as string;
} else if (typeof error === 'object') {
entry.description = error.message;
entry.stack = error.stack;
}
this.errors.push(entry);
if (this.errors.length > maxElements) {
this.errors = this.errors.slice(this.errors.length - maxElements);
}
localStorage.setItem('error_data', JSON.stringify(this.errors));
}
}
The service holds an array of error entries as a private field. If available, these are loaded from LocalStorage as a json array when the service is created.
The logError method writes an error to the client log and stores the error in the array. If the size of the array (maxElement) is exceeded, the oldest entry is removed with slice(). The array is then stored in LocalStorage again.
The getErrors() method returns all error entries as observables. This allows you to display a table with errors in a component in the user interface, if you wish.
So the first two requirements have already been met.
Notification of the user
At this point I decided to use so-called toasts. Toasts are incoming messages that disappear over time (if you want). For this I opted for the MessageService from PrimeNG¹. PrimeNG offers many controls for Angular that go beyond the functionality of HTML5. Therefore I extended the ErrorService class with a method called handleError() and injected the MessageService from PrimeNG:
constructor(private readonly messageService: MessageService) {
const errorData = localStorage.getItem('error_data');
if (errorData) {
this.errors = JSON.parse(errorData) as ErrorEntry[];
} else {
this.errors = [];
}
}
handleError(message: string, error: Error, sticky?: boolean): void {
this.messageService.add({
severity: 'error',
summary: message,
sticky: sticky,
detail: error.message
});
this.logError(error);
}
- The error message is added to the array.
- All errors are stored in LocalStorage.
- Messages are displayed for the user.
Of course you can replace step 3 with your own notification.
Next, you can use the ErrorService at the two places discussed above:
//ErrorService in die entsprechende Komponente injecten, dann:
this.myService.getMyData()
.subscribe(data => this.handleData(data),
error => this.errorService.handleError(‘Error Loading data‘, error));
//Globaler Error-Handler
@Injectable()
export class GlobalErrorHandler implements ErrorHandler {
constructor(private readonly errorService: ErrorService,
private readonly ngZone: NgZone) {}
handleError(error: any) {
this.ngZone.run(() => this.errorService.handleError('Error occurred', error, true));
}
}
Maybe you’re wondering what ngZone.run does? The global ErrorHandler runs outside the Angular zone. This means that no changes are detected and therefore no toast would be displayed until you change the app e.g. by displaying another component. For this reason, you must do this manually at this point.
Exporting the Error List
One request is still missing: the export of the error list as a file. For example, a user could download the file at the push of a button and then forward it by e-mail. For downloading files there is a useful library called ngx-filesaver. This library provides a FileSaverService. Together with the ErrorService the export can be realized easily. In the following code snippet I have illustrated a minimalistic example for the implementation:
import { Component } from '@angular/core';
import { FileSaverService } from 'ngx-filesaver';
import { ErrorService } from 'services/error.service';
@Component({
selector: 'app-error-log',
template: '<button (click)="exportClientLog()">Download error log</button>'
})
export class ErrorLogComponent {
constructor(private readonly errorService: ErrorService,
private readonly fileSaverService: FileSaverService) {
exportClientLog() {
this.errorService.getErrors()
.subscribe(e => {
const text = JSON.stringify(e);
this.fileSaverService.saveText(text, 'client-log.json');
});
}
}
What do you think of the described way to solve the four requirements? Would you have chosen a different path?
Notes:
Peter Friedland has published additional articles here in the t2informatik Blog, including
Peter Friedland
Software Consultant at t2informatik GmbH
Peter Friedland works at t2informatik GmbH as a software consultant. In various customer projects he develops innovative solutions in close cooperation with partners and local contact persons. And from time to time he also blogs.