Forventning om det uventede – bedste praksis for fejlhåndtering i Angular

“At forvente det uventede er udtryk for et helt igennem moderne intellekt.” – Oscar Wilde

Denne artikel handler om centralisering af fejlhåndtering i Angular. Jeg diskuterer nogle af de mere almindelige emner som:

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

Jeg præsenterer nogle kodestumper undervejs og giver til sidst et link til det fulde eksempel.

Spansk version:

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

Hvem skal vi give skylden for fejl?Link til dette afsnit

Hvorfor har vi fejl i vores applikationer? Hvorfor kan vi ikke skrive kode ud fra specifikationer, der altid virker?

I sidste ende er det mennesker, der skaber software, og vi er tilbøjelige til at begå fejl. Nogle af årsagerne til fejl kan være:

  1. Anvendelsens kompleksitet
  2. Kommunikation mellem interessenter
  3. Udviklerfejl
  4. Tidspres
  5. Mangel på testning

Denne liste kunne fortsætte i det uendelige. Med dette i baghovedet kommer det tidspunkt, hvor det uventede sker, og en fejl bliver kastet.

Fange dem, hvis du kanLink til dette afsnit

For at fange synkrone undtagelser i koden kan vi tilføje en try/catch blok. Hvis der kastes en fejl inde i try, så catch vi den og håndterer den. Hvis vi ikke gør det, stopper scriptudførelsen.

<>Kopiering

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

Underforståeligt nok bliver dette meget hurtigt uholdbart. Vi kan ikke forsøge at fange fejl overalt i koden. Vi har brug for global fejlhåndtering.

Catch’em allLink til dette afsnit

Glægeligvis tilbyder Angular en krog til centraliseret undtagelseshåndtering med ErrorHandler.

Den standardimplementering af ErrorHandler udskriver fejlmeddelelser til console.

Vi kan ændre denne adfærd ved at oprette en klasse, der implementerer ErrorHandler:

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

Derpå leverer vi den i vores rodmodul for at ændre standardadfærden i vores applikation. I stedet for at bruge standard ErrorHandler-klassen bruger vi vores klasse.

<>Copy

@NgModule({ providers: })

Så nu har vi kun ét sted, hvor vi skal ændre koden til fejlhåndtering.

Fejl på klientsidenLink til dette afsnit

Når der sker noget uventet på klientsiden, kastes der en JavaScript Error på klientsiden. Den har to vigtige egenskaber, som vi kan bruge.

  1. message – Menneskeligt læsbar beskrivelse af fejlen.
  2. stack – Fejlstack trace med en historik (call stack) over, hvilke filer der var “ansvarlige” for at forårsage denne fejl.

Typisk er message-egenskaben det, vi viser brugeren, hvis vi ikke skriver vores fejlmeddelelser.

Fejl på serversidenLink til dette afsnit

På serversiden returneres et HttpErrorResponse, når noget går galt, på server-siden. Som med JavaScript-fejlen har den en beskedegenskab, som vi kan bruge til meddelelser.

Den returnerer også fejlens statuskode. Disse kan være af forskellige typer. Hvis den starter med et fire (4xx), har klienten gjort noget uventet. Hvis vi f.eks. får status 400 (Bad Request), så var den anmodning, som klienten sendte, ikke det, som serveren forventede.

Statusser, der starter med fem (5xx), er serverfejl. Den mest typiske er 500 Internal Server Error, en meget generel HTTP-statuskode, der betyder, at noget er gået galt på serveren, men at serveren ikke kunne være mere specifik med hensyn til, hvad det nøjagtige problem er.

Med forskellige typer fejl er det nyttigt med en tjeneste, der analyserer meddelelser og stack traces fra dem.

FejlertjenesteLink til dette afsnit

I denne tjeneste tilføjer vi logikken til at analysere fejlmeddelelser og stack traces fra serveren og klienten. Dette eksempel er meget forenklet. For mere avancerede brugssituationer kan vi bruge noget som stacktrace.js.

Logikken i denne tjeneste afhænger af, hvilken type fejl vi modtager fra vores backend. Det afhænger også af, hvilken slags besked vi ønsker at vise vores brugere.

Usuelt set viser vi ikke stack trace’en til vores brugere. Men hvis vi ikke er i et produktionsmiljø, vil vi måske gerne vise stack trace’en til testerne. I det scenarie kan vi indstille et flag, der viser stacktrace.

<>Kopier

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 til dette afsnit

HttpInterceptor blev introduceret med Angular 4.3.1. Den giver mulighed for at opsnappe HTTP-forespørgsler og -svar for at transformere eller håndtere dem, før de sendes videre.

Der er to anvendelsestilfælde, som vi kan implementere i interceptoren.

Først kan vi prøve HTTP-opkaldet igen én eller flere gange, før vi kaster fejlen. I nogle tilfælde, f.eks. hvis vi får en timeout, kan vi fortsætte uden at kaste undtagelsen.

Til dette bruger vi retry-operatoren fra RxJS til at genindmelde os til observablen.

Mere avancerede eksempler på denne form for adfærd:

  • Retry en observerbar sekvens ved fejl baseret på brugerdefinerede kriterier
  • Kraft af RxJS ved brug af eksponentiel backoff

Vi kan derefter kontrollere undtagelsens status og se, om der er tale om en 401 uautoriseret fejl. Med tokenbaseret sikkerhed kan vi forsøge at opdatere tokenet. Hvis dette ikke virker, kan vi omdirigere brugeren til loginsiden.

<>Kopiering

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

Her prøver vi igen én gang, før vi kontrollerer fejlstatus og genindkalder fejlen. Opdatering af sikkerhedstokens ligger uden for denne artikels anvendelsesområde.

Vi skal også levere den interceptor, vi har oprettet.

<>Kopier

providers:

MeddelelserLink til dette afsnit

Til meddelelser bruger jeg Angular Material Snackbar.

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

Med dette har vi simple notifikationer til brugeren, når der opstår fejl.

Vi kan håndtere fejl på server- og klientsiden forskelligt. I stedet for notifikationer kan vi vise en fejlside.

FejlemeldingLink til dette afsnit

Fejlemeldinger er vigtige og bør derfor have en vis betydning for at hjælpe brugeren til at komme videre. Ved at vise “Der er opstået en fejl” fortæller vi ikke brugeren, hvad problemet er, eller hvordan man løser det.

I sammenligning hermed, hvis vi i stedet viser noget som “Beklager, du har ikke flere penge.”, så ved brugeren, hvad fejlen er. Lidt bedre, men det hjælper dem ikke til at løse fejlen.

En endnu bedre løsning ville være at fortælle dem, at de skal overføre flere penge og give et link til en side for pengeoverførsel.

Husk, at fejlhåndtering ikke er en erstatning for dårlig UX.

Det jeg mener med dette er, at du ikke bør have nogen forventede fejl. Hvis en bruger kan gøre noget, der udløser en fejl, skal du rette det!

Lad ikke en fejl slippe igennem, bare fordi du har lavet en flot fejlmeddelelse til den.

LogningLink til dette afsnit

Hvis vi ikke logger fejl, er det kun den bruger, der løber ind i dem, der kender til dem. Det er nødvendigt at gemme oplysningerne for at kunne fejlfinde problemet senere.

Når vi har besluttet at gemme dataene, skal vi også vælge, hvordan vi vil gemme dem. Mere om det senere.

Hvor skal vi gemme dataene?

Med centraliseret fejlhåndtering behøver vi ikke at være alt for kede af at lade beslutningen ligge til senere. Vi har kun ét sted at ændre vores kode nu. For nu skal vi logge meddelelsen til konsollen.

<>Kopier

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

FejlsporingLink til dette afsnit

Det er vigtigt, at du kan identificere fejl i din webapplikation, før brugerne støder på dem. Fejlsporing er processen med proaktivt at identificere problemer og rette dem så hurtigt som muligt.

Så vi kan ikke bare læne os tilbage og forvente, at brugerne rapporterer fejl til os. I stedet bør vi være proaktive ved at logge og overvåge fejl.

Vi bør vide om fejl, når de opstår.

Vi kunne oprette vores løsning til dette formål. Men hvorfor genopfinde hjulet, når der findes så mange fremragende tjenester som Bugsnag, Sentry, TrackJs og Rollbar, der har specialiseret sig i dette område.

Ved hjælp af en af disse front-end-løsninger til fejlsporing kan du optage og afspille brugersessioner, så du selv kan se præcis, hvad brugeren oplevede.

Hvis du ikke kan reproducere en fejl, kan du ikke løse den.

Med andre ord kan en ordentlig løsning til fejlsporing advare dig, når der opstår en fejl, og give dig indsigt i, hvordan du kan replikere/løse problemet.

I en tidligere artikel, Sådan sender du fejl til Slack i Angular, talte jeg om at bruge Slack til at spore fejl. Som et eksempel kunne vi bruge det her:

<>Kopier

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

Implementering af en mere robust løsning ligger uden for rammerne af denne artikel.

Alt sammen nuLink til dette afsnit

Da fejlhåndtering er vigtig, bliver den indlæst først. På grund af dette kan vi ikke bruge afhængighedsinjektion i konstruktøren for tjenesterne. I stedet skal vi injicere dem manuelt med Injector.

<>Kopier

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

SlutningLink til dette afsnit

Fejlhåndtering er en hjørnesten for en virksomhedsapplikation. I denne artikel centraliserede vi den ved at overstyre standardadfærden for ErrorHandler. Derefter tilføjede vi flere tjenester:

  1. Error service til at analysere meddelelser og stack traces.
  2. Notification service til at underrette brugere om fejl.
  3. Logging service til at logge fejl.

Vi implementerede også en interceptor-klasse til:

  1. Retry før fejlen kastes.
  2. Tjekke efter specifikke fejl og reagere i overensstemmelse hermed.

Med denne løsning kan du begynde at spore dine fejl og forhåbentlig give brugerne en bedre oplevelse.

Eksempelkode på GitHub.
Kør koden på StackBlitz.

RessourcerLink til dette afsnit

  • Fejlhåndtering & Angular af Aleix Suau
  • Håndtering af fejl i Javascript: The Definitive Guide af Lukas Gisder-Dubé
  • Global Error Handling with Angular 2+ af Austin
  • Angular applications – error handling and elegant recovery af Brachi Packter
  • The Art of the Error Message af Marina Posniak
  • JavaScript Weekly: Graceful Error Handling af Severin Perez

Diskuter med fællesskabet

Leave a Reply