Att förvänta sig det oväntade – bästa praxis för felhantering i Angular

”Att förvänta sig det oväntade visar på ett helt igenom modernt intellekt.” – Oscar Wilde

Denna artikel handlar om centralisering av felhantering i Angular. Jag diskuterar några av de vanligaste ämnena som:

  • klientfel
  • serverfel
  • användarmeddelande
  • spårningsfel

Jag presenterar några kodutdrag under vägen och ger slutligen en länk till det fullständiga exemplet.

Spansk version:

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

Vem ska vi skylla på för felen?Länk till det här avsnittet

Varför har vi fel i våra applikationer? Varför kan vi inte skriva kod från specifikationer som alltid fungerar?

I slutändan är det människor som skapar programvara och vi är benägna att göra misstag. Några orsaker till fel kan vara:

  1. Applikationens komplexitet
  2. Kommunikation mellan intressenter
  3. Utvecklarens misstag
  4. Tidspress
  5. Misslyckande med testning

Listan skulle kunna fortsätta i oändlighet. Med detta i åtanke kommer tiden då det oväntade inträffar och ett fel kastas.

Fånga dem om du kanLänk till detta avsnitt

För att fånga upp synkrona undantag i koden kan vi lägga till ett try/catch block. Om ett fel kastas inuti trycatch vi det och hanterar det. Om vi inte gör detta stannar exekveringen av skriptet.

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

Underförståeligt nog blir detta ohållbart väldigt snabbt. Vi kan inte försöka fånga upp fel överallt i koden. Vi behöver global felhantering.

Catch’em allLänk till det här avsnittet

Turligtvis tillhandahåller Angular en krok för centraliserad undantagshantering med ErrorHandler.

Standardimplementationen av ErrorHandler skriver ut felmeddelanden till console.

Vi kan ändra det här beteendet genom att skapa en klass som implementerar ErrorHandler:

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

Därefter tillhandahåller vi den i vår rotmodul för att ändra standardbeteendet i vår applikation. Istället för att använda standardklassen ErrorHandler använder vi vår klass.

<>Copy
@NgModule({ providers: })

Så nu har vi bara ett ställe där vi ska ändra koden för felhantering.

Fel på klientsidanLänk till det här avsnittet

Om något oväntat händer på klientsidan kastas ett JavaScript Error. Det har två viktiga egenskaper som vi kan använda.

  1. message – Människoläsbar beskrivning av felet.
  2. stack – Error stack trace (spårning av felet) med en historik (anropsstack) över vilka filer som var ”ansvariga” för att orsaka detta fel.

Typiskt sett är meddelandeegenskapen vad vi visar användaren om vi inte skriver våra felmeddelanden.

Fel på serversidanLänk till det här avsnittet

På serversidan returneras ett HttpErrorResponse när något går fel. Precis som för JavaScript-felet har det en meddelandeegenskap som vi kan använda för meddelanden.

Det returnerar också felets statuskod. Dessa kan vara av olika typer. Om den börjar med en fyra (4xx) har klienten gjort något oväntat. Om vi till exempel får statusen 400 (Bad Request) så var den begäran som klienten skickade inte vad servern förväntade sig.

Statuser som börjar med fem (5xx) är serverfel. Det mest typiska är 500 Internal Server Error, en mycket allmän HTTP-statuskod som innebär att något har gått fel på servern, men att servern inte kunde vara mer specifik om vad det exakta problemet är.

Med olika typer av fel är det till hjälp med en tjänst som analyserar meddelanden och stacktraces från dem.

FelttjänstLänk till det här avsnittet

I den här tjänsten lägger vi till logiken för att analysera felmeddelanden och stacktraces från server och klient. Det här exemplet är mycket förenklat. För mer avancerade användningsfall kan vi använda något som stacktrace.js.

Logiken i den här tjänsten beror på vilken typ av fel vi får från vår backend. Den beror också på vilken typ av meddelande vi vill visa för våra användare.

Ovanligtvis visar vi inte stacktrace för våra användare. Men om vi inte befinner oss i en produktionsmiljö kanske vi vill visa stacktrace för testarna. I det scenariot kan vi sätta en flagga som visar stacktrace.

<>Kopiera
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'; }}

HttpInterceptorLänk till det här avsnittet

HttpInterceptor introducerades med Angular 4.3.1. Den erbjuder ett sätt att fånga upp HTTP-begäranden och -svar för att omvandla eller hantera dem innan de skickas vidare.

Det finns två användningsfall som vi kan implementera i interceptorn.

För det första kan vi försöka igen HTTP-samtalet en eller flera gånger innan vi kastar felet. I vissa fall, till exempel om vi får en timeout, kan vi fortsätta utan att kasta undantaget.

För detta använder vi retry-operatorn från RxJS för att skriva om till observabeln.

Mer avancerade exempel på den här typen av beteende:

  • Retry an observable sequence on error based on custom criteria
  • Power of RxJS when using exponential backoff

Vi kan sedan kontrollera statusen på undantaget och se om det är ett 401 unauthorized error. Med tokenbaserad säkerhet kan vi försöka uppdatera tokenet. Om detta inte fungerar kan vi omdirigera användaren till inloggningssidan.

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

Här gör vi ett nytt försök en gång innan vi kontrollerar felstatusen och återkallar felet. Uppdatering av säkerhetstoken ligger utanför den här artikelns räckvidd.

Vi måste också tillhandahålla den interceptor vi skapade.

<>Copy
providers:

MeddelandenLänk till det här avsnittet

För meddelanden använder jag Angular Material Snackbar.

<>Kopiera
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 detta har vi enkla meddelanden till användaren när fel uppstår.

Vi kan hantera fel på server- och klientsidan på olika sätt. Istället för meddelanden kan vi visa en felsida.

FelmeddelandeLänk till det här avsnittet

Felmeddelanden har betydelse och bör därför ha en viss innebörd för att hjälpa användaren att gå vidare. Genom att visa ”Ett fel inträffade” talar vi inte om för användaren vad problemet är eller hur det ska lösas.

I jämförelse med detta, om vi i stället visar något i stil med ”Ledsen, du har slut på pengar.” så vet användaren vad felet är. Lite bättre men det hjälper dem inte att lösa felet.

En ännu bättre lösning skulle vara att tala om för dem att överföra mer pengar och ge en länk till en sida för överföring av pengar.

Håll i minnet att felhantering inte är en ersättning för dålig UX.

Vad jag menar med det här är att du inte bör ha några förväntade fel. Om en användare kan göra något som ger upphov till ett fel, åtgärda det!

Låt inte ett fel passera bara för att du har skapat ett trevligt felmeddelande för det.

LoggningLänk till det här avsnittet

Om vi inte loggar felen så är det bara den användare som råkar ut för dem som känner till dem. Att spara informationen är nödvändigt för att kunna felsöka problemet senare.

När vi har bestämt oss för att lagra data måste vi också välja hur vi ska spara dem. Mer om det senare.

Var ska vi spara uppgifterna?

Med centraliserad felhantering behöver vi inte känna oss alltför ledsna för att vi lämnar beslutet till senare. Vi har bara ett ställe att ändra vår kod på nu. För tillfället loggar vi meddelandet till konsolen.

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

FelspårningLänk till det här avsnittet

Idealfallet vill du identifiera buggar i ditt webbprogram innan användarna stöter på dem. Felspårning är processen för att proaktivt identifiera problem och åtgärda dem så snabbt som möjligt.

Så, vi kan inte bara luta oss tillbaka och förvänta oss att användarna rapporterar fel till oss. Istället bör vi vara proaktiva genom att logga och övervaka fel.

Vi bör känna till fel när de inträffar.

Vi kan skapa vår lösning för detta ändamål. Men varför uppfinna hjulet på nytt när det finns så många utmärkta tjänster som Bugsnag, Sentry, TrackJs och Rollbar som specialiserar sig på detta område.

Om du använder en av dessa lösningar för felspårning i fronten kan du spela in och spela upp användarsessioner så att du själv kan se exakt vad användaren upplevde.

Om du inte kan reproducera ett fel kan du inte åtgärda det.

Med andra ord kan en ordentlig lösning för felspårning varna dig när ett fel inträffar och ge dig insikter om hur du kan replikera/lösa problemet.

I en tidigare artikel, How to send Errors into Slack in Angular, talade jag om att använda Slack för att spåra fel. Som ett exempel kan vi använda det här:

<>Kopiera

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

Införandet av en mer robust lösning ligger utanför den här artikelns räckvidd.

Allt tillsammans nuLänk till det här avsnittet

Då felhantering är väsentlig, laddas den först. På grund av detta kan vi inte använda beroendeinjektion i konstruktören för tjänsterna. Istället måste vi injicera dem manuellt med Injector.

<>Kopiera

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

SlutsatsLänk till det här avsnittet

Felhantering är en hörnsten för en företagsapplikation. I den här artikeln har vi centraliserat den genom att åsidosätta standardbeteendet för ErrorHandler. Vi lade sedan till flera tjänster:

  1. Felstjänst för att analysera meddelanden och stacktraces.
  2. Bekräftelsetjänst för att meddela användare om fel.
  3. Loggtjänst för att logga fel.

Vi implementerade också en interceptor-klass för att:

  1. Returera innan felet kastas.
  2. Kontrollera för specifika fel och reagera i enlighet därmed.

Med den här lösningen kan du börja spåra dina fel och förhoppningsvis ge användarna en bättre upplevelse.

Exempelkod på GitHub.
Kör koden på StackBlitz.

KällorLänk till det här avsnittet

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

Diskutera med andra användare

Leave a Reply