Expecting the Unexpected – Best practices for Error handling in Angular

“To expect the unexpected shows a thoroughly modern intellect.” – Oscar Wilde

Dit artikel gaat over het centraliseren van foutafhandeling in Angular. Ik bespreek enkele van de meest voorkomende onderwerpen zoals:

  • client-side errors
  • server-side errors
  • user notification
  • tracking errors

Ik presenteer onderweg enkele code snippets en geef tot slot een link naar het volledige voorbeeld.

Spaanse versie:

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

Wie moeten we de schuld geven van fouten? Link naar deze sectie

Waarom hebben we fouten in onze applicaties? Waarom kunnen we geen code schrijven op basis van specificaties die altijd werkt?

Eindelijk maken mensen software, en zijn we geneigd om fouten te maken. Enkele redenen achter fouten kunnen zijn:

  1. Complexiteit van de applicatie
  2. Communicatie tussen belanghebbenden
  3. Fouten van de ontwikkelaar
  4. Tijddruk
  5. Tekort aan testen

Deze lijst zou nog wel even door kunnen gaan. Met dit in gedachten, komt de tijd dat het onverwachte gebeurt, en een fout wordt gegooid.

Vang ze als je kuntLink naar deze sectie

Om synchrone uitzonderingen in de code op te vangen, kunnen we een try/catch blok toevoegen. Als een fout wordt gegooid binnen try dan catch we het en behandelen het. Als we dit niet doen, stopt de uitvoering van het script.

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

Onbegrijpelijkerwijs wordt dit heel snel onhoudbaar. We kunnen niet proberen om overal in de code fouten op te vangen. We hebben wereldwijde foutafhandeling nodig.

Catch’em allLink to this section

Gelukkig biedt Angular een haak voor gecentraliseerde uitzonderingsafhandeling met ErrorHandler.

De standaardimplementatie van ErrorHandler drukt foutmeldingen af naar de console.

We kunnen dit gedrag wijzigen door een klasse te maken die de ErrorHandler implementeert:

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

Daarna kunnen we deze in onze basismodule opnemen om het standaardgedrag in onze toepassing te wijzigen. In plaats van de standaard ErrorHandler class te gebruiken, gebruiken we onze class.

<>Copy
@NgModule({ providers: })

Dus nu hebben we nog maar één plaats waar we de code voor foutafhandeling moeten veranderen.

Client-side errorsLink to this section

Aan de client-kant wordt, wanneer er iets onverwachts gebeurt, een JavaScript Error gegooid. Het heeft twee belangrijke eigenschappen die we kunnen gebruiken.

  1. message – Menselijk leesbare beschrijving van de fout.
  2. stack – Error stack trace met een geschiedenis (call stack) van welke bestanden ‘verantwoordelijk’ waren voor het veroorzaken van die Error.

Typisch is de message property wat we de gebruiker laten zien als we onze foutmeldingen niet schrijven.

Server-side errorsLink naar deze sectie

Aan de server-side wordt, als er iets fout gaat, een HttpErrorResponse geretourneerd. Net als bij de JavaScript-fout, heeft deze een message property die we kunnen gebruiken voor notificaties.

Het retourneert ook de statuscode van de fout. Deze kan van verschillende types zijn. Als het begint met een vier (4xx), dan heeft de client iets onverwachts gedaan. Bijvoorbeeld, als we de status 400 (Bad Request), dan is het verzoek dat de client verzonden was niet wat de server verwacht.

Statussen die beginnen met een vijf (5xx) zijn server fouten. De meest typische is de 500 Internal Server Error, een zeer algemene HTTP-statuscode die betekent dat er iets mis is gegaan op de server, maar de server kon niet specifieker zijn over wat het exacte probleem is.

Met verschillende soorten fouten, is het handig met een service die berichten en stack traces van hen parseert.

Error serviceLink naar deze sectie

In deze service voegen we de logica toe voor het parsen van foutberichten en stack traces van de server en de client. Dit voorbeeld is erg simplistisch. Voor geavanceerdere gevallen zouden we iets als stacktrace.js kunnen gebruiken.

De logica in deze service hangt af van wat voor soort fouten we van onze backend ontvangen. Het hangt ook af van wat voor soort bericht we aan onze gebruikers willen laten zien.

Normaal gesproken laten we de stacktrace niet aan onze gebruikers zien. Echter, als we niet in een productie-omgeving, willen we misschien de stack trace laten zien aan de testers. In dat geval kunnen we een vlag instellen die de stack trace toont.

<>Kopie
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 naar deze sectie

HttpInterceptor is geïntroduceerd met Angular 4.3.1. Het biedt een manier om HTTP requests en responses te onderscheppen om ze te transformeren of af te handelen voordat ze worden doorgegeven.

Er zijn twee use cases die we kunnen implementeren in de interceptor.

Eerst kunnen we de HTTP call één of meerdere keren opnieuw proberen voordat we de fout gooien. In sommige gevallen, bijvoorbeeld als we een time-out krijgen, kunnen we doorgaan zonder de exception te gooien.

Voor dit, gebruiken we de retry operator van RxJS om ons opnieuw aan te melden bij de observable.

Meer geavanceerde voorbeelden van dit soort gedrag:

  • Retry een observeerbare reeks op fout op basis van aangepaste criteria
  • Kracht van RxJS bij gebruik van exponentiële backoff

We kunnen dan de status van de uitzondering controleren en zien of het een 401 ongeoorloofde fout is. Met token-gebaseerde beveiliging, kunnen we proberen om het token te vernieuwen. Als dit niet werkt, kunnen we de gebruiker doorverwijzen naar de inlogpagina.

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

Hierna proberen we het een keer opnieuw voordat we de foutstatus controleren en de fout opnieuw afwijzen. Het vernieuwen van beveiligingstokens valt buiten het bestek van dit artikel.

We moeten ook de interceptor leveren die we hebben gemaakt.

<>Copy
providers:

MeldingenLink naar dit gedeelte

Voor meldingen maak ik gebruik van Angular Material Snackbar.

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

Hiermee hebben we eenvoudige meldingen voor de gebruiker wanneer er fouten optreden.

We kunnen server-side en client-side fouten verschillend afhandelen. In plaats van meldingen kunnen we een foutpagina laten zien.

FoutmeldingLink naar deze sectie

Foutmeldingen zijn belangrijk en moeten daarom enige betekenis hebben om de gebruiker verder te helpen. Door “Er is een fout opgetreden” te tonen, vertellen we de gebruiker niet wat het probleem is of hoe hij het kan oplossen.

In vergelijking daarmee, als we in plaats daarvan iets tonen als “Sorry, u hebt geen geld meer.”, dan weet de gebruiker wat de fout is. Een beetje beter, maar het helpt ze niet om de fout op te lossen.

Een nog betere oplossing zou zijn om ze te vertellen dat ze meer geld moeten overmaken en een link te geven naar een pagina voor het overmaken van geld.

Bedenk dat foutafhandeling geen vervanging is voor slechte UX.

Wat ik hiermee bedoel is dat je geen verwachte fouten zou moeten hebben. Als een gebruiker iets kan doen dat een fout oplevert, los het dan op!

Laat een fout niet door, alleen omdat je er een mooie foutmelding voor hebt gemaakt.

LoggingLink naar deze sectie

Als we fouten niet loggen, dan weet alleen de gebruiker die er tegenaan loopt ervan. Het opslaan van de informatie is noodzakelijk om later het probleem te kunnen oplossen.

Wanneer we hebben besloten de gegevens op te slaan, moeten we ook kiezen hoe we ze opslaan. Daarover later meer.

Waar moeten we de gegevens opslaan?

Met gecentraliseerde foutafhandeling hoeven we niet al te veel spijt te hebben dat we de beslissing voor later hebben gelaten. We hoeven onze code nu nog maar op één plaats aan te passen. Voorlopig loggen we het bericht naar de console.

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

FoutopsporingLink naar deze sectie

In het algemeen wilt u bugs in uw webapplicatie opsporen voordat gebruikers ermee in aanraking komen. Error tracking is het proces van pro-actief identificeren van problemen en deze zo snel mogelijk oplossen.

Dus, we kunnen niet achterover leunen en verwachten dat gebruikers fouten aan ons melden. In plaats daarvan moeten we proactief zijn door fouten te loggen en te controleren.

We moeten op de hoogte zijn van fouten wanneer ze optreden.

We zouden onze oplossing hiervoor kunnen maken. Maar waarom zouden we het wiel opnieuw uitvinden als er zoveel uitstekende diensten zijn, zoals Bugsnag, Sentry, TrackJs en Rollbar, die gespecialiseerd zijn op dit gebied.

Door een van deze front-end oplossingen voor foutopsporing te gebruiken, kunt u gebruikerssessies opnemen en opnieuw afspelen, zodat u met eigen ogen kunt zien wat de gebruiker precies heeft ervaren.

Als je een fout niet kunt reproduceren, kun je hem ook niet oplossen.

Met andere woorden, een goede oplossing voor het traceren van fouten kan je waarschuwen wanneer een fout optreedt en inzicht geven in hoe je het probleem kunt reproduceren/oplossen.

In een eerder artikel, Hoe stuur ik fouten naar Slack in Angular, heb ik het gehad over het gebruik van Slack om fouten te traceren. Als voorbeeld zouden we het hier kunnen gebruiken:

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

Het implementeren van een robuustere oplossing valt buiten het bestek van dit artikel.

Alles bij elkaar nuLink naar deze sectie

Omdat foutafhandeling essentieel is, wordt het als eerste geladen. Daarom kunnen we geen gebruik maken van dependency injection in de constructor voor de services. In plaats daarvan moeten we ze handmatig injecteren met Injector.

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

ConclusieLink naar deze sectie

Foutafhandeling is een hoeksteen voor een bedrijfstoepassing. In dit artikel hebben we dit gecentraliseerd door het standaardgedrag van ErrorHandler te overschrijven. Vervolgens hebben we verschillende services toegevoegd:

  1. Error service om berichten en stack traces te ontleden.
  2. Notification service om gebruikers te informeren over fouten.
  3. Logging service om fouten te loggen.

We hebben ook een interceptor class geïmplementeerd om:

  1. Retry te doen voordat de fout wordt gegooid.
  2. Check voor specifieke fouten en dienovereenkomstig te reageren.

Met deze oplossing, kunt u beginnen met het bijhouden van uw fouten en hopelijk geven de gebruikers een betere ervaring.

Example code op GitHub.
Run de code op StackBlitz.

ResourcesLink naar deze sectie

  • Error Handling & Angular door Aleix Suau
  • Handling Errors in Javascript: The Definitive Guide door Lukas Gisder-Dubé
  • Global Error Handling with Angular 2+ door Austin
  • Angular applicaties – foutafhandeling en elegant herstel door Brachi Packter
  • The Art of the Error Message door Marina Posniak
  • JavaScript Weekly: Graceful Error Handling door Severin Perez

Discussieer met de gemeenschap

Leave a Reply