Fehlerbehandlung in Angular-Anwendungen
- die Zerlegung der Anwendung in wiederverwendbare Komponenten,
- Services als Abstraktionsschicht für Business-Logik und/oder Datenbeschaffung,
- die Modularisierung von Komponenten und Services mittels Angular Module,
- Depedency Injection und IoC Contrainer,
- Commandozeilen-Tool „Angular CLI“, insbesondere
Live-Entwicklung mittels ng serve,
build mittels Webpack, inklusive Treeshaking und Minification,
Ausführen von Unit Tests, - die Erstellung von Libraries für applikationsübergreifende Wiederverwendung,
- sowie Angular Elements zur Umsetzung von Web-Komponenten
Es gibt sicherlich sehr viel Positives über Angular und verschiedene Anwendungsfälle zu berichten. Ich möchte Ihnen heute einen Vorschlag zur Fehlerbehandlung in Angular-Anwendungen unterbreiten.
Die Anforderungen vom Logging bis zum Export
Es gibt verschiedene Wege, um sich dem Thema Fehlerbehandlung auf Clients zu nähern. Letztendlich funktionieren diese Wege auch oftmals unabhängig von der gewählten Technologie. Es ist also sekundär,ob es sich um eine native Anwendung wie Word, Excel, Adobe Photoshop oder eine Webanwendung wie Google Mail handelt. In einem meiner Projekte habe ich die folgende, wesentlichen Anforderungen identifiziert:
- Alle unerwarteten Fehler werden geloggt, persistiert und können im Client abgerufen werden.
- Das Abfangen von Fehlern sollte möglichst einfach und kontinuierlich funktionieren.
- Der Nutzer sollte auf diese Fehler aufmerksam gemacht werden können, sei es durch Benachrichtigungen und/oder Messageboxen.
- Die Fehlerliste muss für Auswertungen exportierbar sein.
Die Identifikation möglicher Fehlerquellen
Für die Umsetzung der genannten Anforderungen ist es wichtig, die Stellen in Ihrer Anwendung zu identifizieren, die Fehler erzeugen könnten. Dazu gehören oftmals subscribe()-Aufrufe auf Services, die einen http-Request absetzen, oder allgemeine Fehler, wie Zugriffe auf undefined/null properties.
Das Subscribe-Fehlerhandling
Bei Subscriptions finden Sie ggf. einen solchen Code vor:
this.myService.getMyData()
.subscribe(data => this.handleData(data),
error => console.log(error));
Hiermit wird jeglicher Fehler, die ein next() auf dem Observable auslöst, in den Browser-Log geschrieben. Prinzipiell hinreichend, aber der Nutzer bekommt bei einem Fehlerfall davon gar nichts mit; bestenfalls fällt ihm auf, dass die Anwendung „komisch“ reagiert.
Der allgemein auftretende Fehler
Theoretisch und auch praktisch kann an jeder Stelle einer Anwendung ein Fehler auftreten. Was tun? Angular bietet hierfür eine schöne Lösung an: die Definition eines sogenannten ErrorHandlers. Immer, wenn ein Fehler nicht abgefangen wird, wird dieser Handler aufgerufen. Alles was Sie dafür tun müssen, ist die Implementierung einer Klasse, die Sie in Ihrer Angular-Anwendung über dem entsprechenden Modul – meistens dürfte es im AppModule Sinn machen – hängen:
class GlobalErrorHandler implements ErrorHandler {
handleError(error: any) {
//hier den Fehler behandeln.
}
}
@NgModule({
providers: [{provide: ErrorHandler, useClass: GlobalErrorHandler}]
})
class AppModule {}
Die Verwendung des ErrorServices
Für die Anforderung ein Problem wiederkehrend einfach zu lösen, bietet sich die Implementierung eines eigenen Services an. Idealerweise würde ich ihn ErrorService nennen. Hier können Sie auch die Fehler des Clients persistieren. In meinem Beispiel habe ich mich entschieden, die letzten 20 Fehler rollierend zu loggen und im LocalStorage zu speichern. Der LocalStorage ist auf wenige Megabytes begrenzt, so dass Sie hier auf den Verbrauch achten sollten. Meine Implementierung sieht wie folgt aus:
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));
}
}
Der Service hält als privates Feld ein Array an Fehlereinträgen. Falls vorhanden werden diese beim Erstellen des Services aus dem LocalStorage als json-array geladen.
Die Methode logError schreibt einen Fehler in das Client-log und speichert den Fehler im Array. Sollte hierbei die Größe des Arrays (maxElements) überschritten werden, wird der älteste Eintrag mittels slice() entfernt. Danach wird das Array wieder im LocalStorage abgelegt.
Die Methode getErrors() liefert alle Erroreinträge als Observable. Damit können Sie ggf. in der Oberfläche eine Tabelle mit Fehlern in einer Komponente anzeigen, sofern Sie das möchten.
Und schon haben Sie die ersten beiden Anforderungen bereits umgesetzt.
Benachrichtigung des Nutzers
Ich habe mich an dieser Stelle entschieden, sogenannte Toasts zu verwenden. Toasts sind einfliegende Nachrichten, die mit der Zeit wieder verschwinden (wenn man das möchte). Dazu habe ich mich für den MessageService von PrimeNG¹ entschieden. PrimeNG bietet für Angular viele Steuerelemente an, die über den Funktionsumfang von HTML5 hinausgehen. Dafür habe ich die Klasse ErrorService um eine Methode namens handleError() erweitert und den MessageService aus PrimeNG injiziert:
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);
}
- Die Fehlernachricht wird ins Array aufgenommen.
- Alle Fehler werden im LocalStorage gespeichert.
- Nachricht werden für den User ausgegeben.
Natürlich können Sie Schritt 3 durch Ihre eigene Benachrichtigung ersetzen.
Als nächstes können Sie den ErrorService an den beiden oben besprochenen Stellen verwenden:
//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));
}
}
Vielleicht fragen Sie sich, was ngZone.run bewerkstelligt? Der globale ErrorHandler läuft außerhalb der Angular-Zone. Das bedeutet, dass keinerlei Änderungen erkannt werden und somit auch kein Toast angezeigt werden würde, bis Sie eine Änderung der App bspw. durch das Anzeigen einer anderen Komponente verändern. Aus diesem Grund müssen Sie dies an dieser Stelle manuell durchführen.
Der Export der Fehlerliste
Es fehlt noch eine Anforderung: der Export der Fehlerliste als Datei. Bspw. könnte ein Nutzer per Knopfdruck die Datei herunterladen und dann per E-Mail weiterleiten. Für den Download von Dateien gibt es eine nützliche Bibliothek namens ngx-filesaver. Diese stellt einen FileSaverService bereit. Gemeinsam mit dem ErrorService lässt sich der Export einfach realisieren. Im folgenden Code-Snippet habe ich ein minimalistisches Beispiel zur Implementierung abgebildet:
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');
});
}
}
Hinweise:
Interessieren Sie sich für weitere Tipps aus der Praxis? Testen Sie unseren wöchentlichen Newsletter mit interessanten Beiträgen, Downloads, Empfehlungen und aktuellem Wissen.
Wir suchen Softwareentwickler. Berufseinsteigerinnen, Entwickler mit einigen und Expertinnen mit vielen Jahren Erfahrung.
Peter Friedland hat im t2informatik Blog einige weitere Beiträge veröffentlicht, u. a.
Peter Friedland
t2informatik GmbH
Peter Friedland ist bei der t2informatik GmbH als Software Consultant tätig. In verschiedenen Kundenprojekten entwickelt er innovative Lösungen in enger Zusammenarbeit mit Partnern und den Ansprechpartnern vor Ort. Und ab und an bloggt er auch.