Das Unerwartete erwarten – Best Practices für die Fehlerbehandlung in Angular

„Das Unerwartete zu erwarten, zeigt einen durch und durch modernen Intellekt.“ – Oscar Wilde

In diesem Artikel geht es um die Zentralisierung der Fehlerbehandlung in Angular. Ich bespreche einige der häufigsten Themen wie:

  • client-seitige Fehler
  • server-seitige Fehler
  • Benutzerbenachrichtigung
  • Fehlertracking

Ich präsentiere einige Codeschnipsel auf dem Weg und biete schließlich einen Link zum vollständigen Beispiel.

Spanische Version:

  • Esperando lo Inesperado – Buenas prácticas para el manejo de errores en Angular

Wem sollten wir die Schuld für Fehler geben?Link zu diesem Abschnitt

Warum haben wir Fehler in unseren Anwendungen? Warum können wir keinen Code aus Spezifikationen schreiben, der immer funktioniert?

Es sind schließlich Menschen, die Software erstellen, und wir neigen dazu, Fehler zu machen. Einige Gründe für Fehler könnten sein:

  1. Komplexität der Anwendung
  2. Kommunikation zwischen den Beteiligten
  3. Fehler des Entwicklers
  4. Zeitdruck
  5. Mangel an Tests

Diese Liste ließe sich beliebig fortsetzen. In diesem Sinne kommt der Zeitpunkt, an dem das Unerwartete passiert und ein Fehler ausgelöst wird.

Fangen Sie sie ab, wenn Sie könnenLink zu diesem Abschnitt

Um synchrone Ausnahmen im Code abzufangen, können wir einen try/catch Block hinzufügen. Wenn ein Fehler innerhalb von try geworfen wird, dann catchbehandeln wir ihn. Wenn wir das nicht tun, stoppt die Skriptausführung.

<>Kopieren
try { throw new Error('En error happened');}catch (error) { console.error('Log error', error);}console.log('Script execution continues');

Dies wird verständlicherweise sehr schnell unhaltbar. Wir können nicht versuchen, überall im Code Fehler abzufangen. Wir brauchen eine globale Fehlerbehandlung.

Catch’em allLink zu diesem Abschnitt

Glücklicherweise bietet Angular mit ErrorHandler einen Hook für eine zentralisierte Ausnahmebehandlung.

Die Standardimplementierung von ErrorHandler gibt Fehlermeldungen in den console.

Wir können dieses Verhalten ändern, indem wir eine Klasse erstellen, die den ErrorHandler implementiert:

<>Kopieren
import { ErrorHandler } from '@angular/core';@Injectable()export class GlobalErrorHandler implements ErrorHandler { handleError(error) { // your custom error handling logic }}

Dann stellen wir sie in unserem Stammmodul bereit, um das Standardverhalten in unserer Anwendung zu ändern. Statt der Standard ErrorHandler-Klasse verwenden wir unsere Klasse.

<>Copy
@NgModule({ providers: })

So haben wir jetzt nur noch eine Stelle, an der wir den Code für die Fehlerbehandlung ändern müssen.

Fehler auf der Client-SeiteLink zu diesem Abschnitt

Auf der Client-Seite wird ein JavaScript-Fehler ausgelöst, wenn etwas Unerwartetes passiert. Er hat zwei wichtige Eigenschaften, die wir verwenden können.

  1. Meldung – Von Menschen lesbare Beschreibung des Fehlers.
  2. Stapel – Fehlerstapelverfolgung mit einer Historie (Aufrufstapel) der Dateien, die für diesen Fehler „verantwortlich“ waren.

Typischerweise ist die message-Eigenschaft das, was wir dem Benutzer zeigen, wenn wir unsere Fehlermeldungen nicht schreiben.

Serverseitige FehlerLink zu diesem Abschnitt

Auf der Serverseite wird, wenn etwas schief geht, eine HttpErrorResponse zurückgegeben. Wie beim JavaScript-Fehler hat sie eine Nachrichteneigenschaft, die wir für Benachrichtigungen verwenden können.

Sie gibt auch den Statuscode des Fehlers zurück. Dieser kann von unterschiedlichem Typ sein. Beginnt er mit einer Vier (4xx), dann hat der Client etwas Unerwartetes getan. Wenn wir zum Beispiel den Status 400 (Bad Request) erhalten, entsprach die vom Client gesendete Anfrage nicht den Erwartungen des Servers.

Statuscodes, die mit fünf (5xx) beginnen, sind Serverfehler. Der typischste ist der 500 Internal Server Error, ein sehr allgemeiner HTTP-Statuscode, der bedeutet, dass etwas auf dem Server schief gelaufen ist, aber der Server konnte nicht genauer sagen, was das genaue Problem ist.

Bei verschiedenen Arten von Fehlern ist es hilfreich, einen Dienst zu haben, der Nachrichten und Stack Traces von ihnen analysiert.

FehlerdienstLink zu diesem Abschnitt

In diesem Dienst fügen wir die Logik für das Parsen von Fehlermeldungen und Stack Traces von Server und Client hinzu. Dieses Beispiel ist sehr simpel. Für fortgeschrittenere Anwendungsfälle könnten wir etwas wie stacktrace.js verwenden.

Die Logik in diesem Dienst hängt davon ab, welche Art von Fehlern wir von unserem Backend erhalten. Es hängt auch davon ab, welche Art von Nachricht wir unseren Benutzern zeigen wollen.

Normalerweise zeigen wir unseren Benutzern den Stacktrace nicht an. Wenn wir uns jedoch nicht in einer Produktionsumgebung befinden, möchten wir den Stack-Trace vielleicht den Testern zeigen. In diesem Szenario können wir ein Flag setzen, das den Stack-Trace anzeigt.

<>Kopieren
import { Injectable } from '@angular/core';import { HttpErrorResponse } from '@angular/common/http';@Injectable({ providedIn: 'root'})export class ErrorService { getClientMessage(error: Error): string { if (!navigator.onLine) { return 'No Internet Connection'; } return error.message ? error.message : error.toString(); } getClientStack(error: Error): string { return error.stack; } getServerMessage(error: HttpErrorResponse): string { return error.message; } getServerStack(error: HttpErrorResponse): string { // handle stack trace return 'stack'; }}

HttpInterceptorLink zu diesem Abschnitt

HttpInterceptor wurde mit Angular 4.3.1 eingeführt. Er bietet eine Möglichkeit, HTTP-Anfragen und -Antworten abzufangen, um sie zu transformieren oder zu verarbeiten, bevor sie weitergegeben werden.

Es gibt zwei Anwendungsfälle, die wir im Interceptor implementieren können.

Erstens können wir den HTTP-Aufruf einmal oder mehrmals wiederholen, bevor wir den Fehler auslösen. In einigen Fällen, z. B. bei einer Zeitüberschreitung, können wir fortfahren, ohne die Exception auszulösen.

Dazu verwenden wir den Retry-Operator von RxJS, um das Observable erneut zu abonnieren.

Ausführlichere Beispiele für diese Art von Verhalten:

  • Wiederholung einer Observable-Sequenz im Fehlerfall basierend auf benutzerdefinierten Kriterien
  • Macht RxJS bei Verwendung von exponentiellem Backoff

Wir können dann den Status der Ausnahme überprüfen und sehen, ob es sich um einen 401 unautorisierten Fehler handelt. Bei tokenbasierter Sicherheit können wir versuchen, das Token zu aktualisieren. Wenn dies nicht funktioniert, können wir den Benutzer auf die Anmeldeseite umleiten.

<>Kopieren
import { Injectable } from '@angular/core';import { HttpEvent, HttpRequest, HttpHandler, HttpInterceptor, HttpErrorResponse } from '@angular/common/http';import { Observable, throwError } from 'rxjs';import { retry, catchError } from 'rxjs/operators';@Injectable()export class ServerErrorInterceptor implements HttpInterceptor { intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { return next.handle(request).pipe( retry(1), catchError((error: HttpErrorResponse) => { if (error.status === 401) { // refresh token } else { return throwError(error); } }) ); }}

Hier versuchen wir es einmal, bevor wir den Fehlerstatus überprüfen und den Fehler erneut auslösen. Das Aktualisieren von Sicherheits-Tokens liegt außerhalb des Rahmens dieses Artikels.

Wir müssen auch den Interceptor bereitstellen, den wir erstellt haben.

<>Copy
providers:

BenachrichtigungenLink zu diesem Abschnitt

Für Benachrichtigungen verwende ich Angular Material Snackbar.

<>Kopieren
import { Injectable} from '@angular/core';import { MatSnackBar } from '@angular/material/snack-bar';@Injectable({ providedIn: 'root'})export class NotificationService { constructor(public snackBar: MatSnackBar) { } showSuccess(message: string): void { this.snackBar.open(message); } showError(message: string): void { // The second parameter is the text in the button. // In the third, we send in the css class for the snack bar. this.snackBar.open(message, 'X', {panelClass: }); }}

Damit haben wir einfache Benachrichtigungen für den Benutzer, wenn Fehler auftreten.

Wir können serverseitige und clientseitige Fehler unterschiedlich behandeln. Anstelle von Benachrichtigungen könnten wir eine Fehlerseite anzeigen.

FehlermeldungLink zu diesem Abschnitt

Fehlermeldungen sind wichtig und sollten daher eine Bedeutung haben, um dem Benutzer zu helfen, weiterzukommen. Wenn wir „Ein Fehler ist aufgetreten“ anzeigen, sagen wir dem Benutzer nicht, was das Problem ist oder wie es zu lösen ist.

Wenn wir stattdessen etwas anzeigen wie „Tut mir leid, Sie haben kein Geld mehr“, dann weiß der Benutzer, was der Fehler ist. Das ist etwas besser, aber es hilft ihm nicht, den Fehler zu beheben.

Eine noch bessere Lösung wäre es, ihm zu sagen, dass er mehr Geld überweisen soll, und einen Link zu einer Geldüberweisungsseite anzugeben.

Denken Sie daran, dass Fehlerbehandlung kein Ersatz für schlechte UX ist.

Was ich damit meine, ist, dass Sie keine erwarteten Fehler haben sollten. Wenn ein Benutzer etwas tun kann, das einen Fehler auslöst, dann beheben Sie es!

Lassen Sie einen Fehler nicht durch, nur weil Sie eine nette Fehlermeldung dafür erstellt haben.

LoggingLink zu diesem Abschnitt

Wenn wir Fehler nicht protokollieren, dann weiß nur der Benutzer, der auf sie stößt, davon. Die Speicherung der Informationen ist notwendig, um das Problem später beheben zu können.

Wenn wir uns entschieden haben, die Daten zu speichern, müssen wir auch entscheiden, wie wir sie speichern wollen. Mehr dazu später.

Wo sollen wir die Daten speichern?

Mit der zentralisierten Fehlerbehandlung müssen wir uns nicht zu schade sein, die Entscheidung auf später zu verschieben. Wir haben jetzt nur noch eine Stelle, an der wir unseren Code ändern können. Für den Moment protokollieren wir die Meldung in der Konsole.

<>Kopieren Sie
import { Injectable } from '@angular/core';@Injectable({ providedIn: 'root'})export class LoggingService { logError(message: string, stack: string) { // Send errors to be saved here // The console.log is only for testing this example. console.log('LoggingService: ' + message); }}

FehlerverfolgungLink zu diesem Abschnitt

Im Grunde möchten Sie Fehler in Ihrer Webanwendung identifizieren, bevor die Benutzer darauf stoßen. Fehlerverfolgung ist der Prozess der proaktiven Identifizierung von Problemen und deren schnellstmöglicher Behebung.

Wir können uns also nicht einfach zurücklehnen und erwarten, dass die Benutzer uns Fehler melden. Stattdessen sollten wir proaktiv handeln, indem wir Fehler protokollieren und überwachen.

Wir sollten über Fehler Bescheid wissen, wenn sie auftreten.

Wir könnten unsere Lösung zu diesem Zweck erstellen. Aber warum das Rad neu erfinden, wenn es so viele hervorragende Dienste wie Bugsnag, Sentry, TrackJs und Rollbar gibt, die sich auf diesen Bereich spezialisiert haben.

Die Verwendung einer dieser Front-End-Fehlerverfolgungslösungen ermöglicht es Ihnen, Benutzersitzungen aufzuzeichnen und wiederzugeben, so dass Sie selbst genau sehen können, was der Benutzer erlebt hat.

Wenn man einen Fehler nicht reproduzieren kann, kann man ihn auch nicht beheben.

Mit anderen Worten, eine geeignete Fehlerverfolgungslösung kann Sie warnen, wenn ein Fehler auftritt, und Ihnen Einblicke geben, wie Sie das Problem reproduzieren/beheben können.

In einem früheren Artikel, How to send Errors into Slack in Angular, habe ich über die Verwendung von Slack zur Fehlerverfolgung gesprochen. Als Beispiel könnten wir es hier verwenden:

<>Kopieren
import { Injectable } from '@angular/core';import { SlackService } from './slack.service';@Injectable({ providedIn: 'root'})export class LoggingService { constructor(private slackService: SlackService) { } logError(message: string, stack: string) { this.slackService.postErrorOnSlack(message, stack); }}

Die Implementierung einer robusteren Lösung liegt außerhalb des Rahmens dieses Artikels.

Alles zusammen

Da die Fehlerbehandlung wesentlich ist, wird sie zuerst geladen. Aus diesem Grund können wir keine Dependency Injection im Konstruktor für die Dienste verwenden. Stattdessen müssen wir sie manuell mit Injector injizieren.

<>Kopieren
import { ErrorHandler, Injectable, Injector } from '@angular/core';import { HttpErrorResponse } from '@angular/common/http';import { LoggingService } from './services/logging.service';import { ErrorService } from './services/error.service';import { NotificationService } from './services/notification.service';@Injectable()export class GlobalErrorHandler implements ErrorHandler { // Error handling is important and needs to be loaded first. // Because of this we should manually inject the services with Injector. constructor(private injector: Injector) { } handleError(error: Error | HttpErrorResponse) { const errorService = this.injector.get(ErrorService); const logger = this.injector.get(LoggingService); const notifier = this.injector.get(NotificationService); let message; let stackTrace; if (error instanceof HttpErrorResponse) { // Server Error message = errorService.getServerMessage(error); stackTrace = errorService.getServerStack(error); notifier.showError(message); } else { // Client Error message = errorService.getClientMessage(error); stackTrace = errorService.getClientStack(error); notifier.showError(message); } // Always log errors logger.logError(message, stackTrace); console.error(error); }}

AbschlussLink zu diesem Abschnitt

Die Fehlerbehandlung ist ein Eckpfeiler für eine Unternehmensanwendung. In diesem Artikel haben wir sie zentralisiert, indem wir das Standardverhalten von ErrorHandler überschrieben haben. Dann haben wir mehrere Dienste hinzugefügt:

  1. Fehlerdienst zum Parsen von Nachrichten und Stack Traces.
  2. Benachrichtigungsdienst, um Benutzer über Fehler zu informieren.
  3. Protokollierungsdienst, um Fehler zu protokollieren.

Wir haben auch eine Abfangklasse implementiert, um:

  1. vor dem Auslösen des Fehlers zu reagieren.
  2. auf bestimmte Fehler zu prüfen und entsprechend zu reagieren.

Mit dieser Lösung können Sie beginnen, Ihre Fehler zu verfolgen und den Benutzern hoffentlich eine bessere Erfahrung zu bieten.

Beispielcode auf GitHub.
Führen Sie den Code auf StackBlitz aus.

RessourcenLink zu diesem Abschnitt

  • Fehlerbehandlung & Angular von Aleix Suau
  • Handling Errors in Javascript: The Definitive Guide von Lukas Gisder-Dubé
  • Global Error Handling with Angular 2+ von Austin
  • Angular applications – error handling and elegant recovery von Brachi Packter
  • The Art of the Error Message von Marina Posniak
  • JavaScript Weekly: Graceful Error Handling von Severin Perez

Diskutieren Sie mit der Community

Leave a Reply