A váratlanra számítva – A hibakezelés legjobb gyakorlatai Angularban

“A váratlanra számítani alaposan modern intellektusról tanúskodik.” – Oscar Wilde

Ez a cikk a hibakezelés központosításáról szól Angularban. Tárgyalok néhány gyakori témát, mint például:

  • kliensoldali hibák
  • szerveroldali hibák
  • felhasználói értesítés
  • követési hibák

Mutatok néhány kódrészletet menet közben, végül pedig megadom a teljes példa linkjét.

Spanyol változat:

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

Kit okoljunk a hibákért?Link erre a részre

Miért vannak hibák az alkalmazásainkban? Miért nem tudunk olyan kódot írni a specifikációkból, ami mindig működik?

Végeredményben emberek készítenek szoftvereket, és hajlamosak vagyunk hibázni. Néhány ok a hibák mögött lehet:

  1. Az alkalmazás bonyolultsága
  2. Az érintettek közötti kommunikáció
  3. A fejlesztő hibái
  4. Az időnyomás
  5. A tesztelés hiánya

A listát még hosszan lehetne folytatni. Ezt szem előtt tartva eljön az idő, amikor a váratlan megtörténik, és hibát dobunk.

Catch them if you canLink to this section

A szinkron kivételek elkapásához a kódban hozzáadhatunk egy try/catch blokkot. Ha a try belsejében hibát dobunk, akkor catch azt és kezeljük. Ha ezt nem tesszük meg, akkor a szkript végrehajtása leáll.

<>Másolás

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

Ez érthető módon nagyon gyorsan fenntarthatatlanná válik. Nem próbálhatjuk mindenhol a kódban a hibákat elkapni. Globális hibakezelésre van szükségünk.

Catch’em allLink erre a szakaszra

Szerencsére az Angular biztosít egy horgot a központosított kivételkezeléshez az ErrorHandlerrel.

A ErrorHandler alapértelmezett implementációja a console hibaüzeneteket írja ki.

Módosíthatjuk ezt a viselkedést egy olyan osztály létrehozásával, amely megvalósítja az ErrorHandler-t:

<>Copy

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

Ezt a gyökérmodulunkban biztosítjuk, hogy megváltoztassuk az alkalmazásunk alapértelmezett viselkedését. Az alapértelmezett ErrorHandler osztály helyett a mi osztályunkat használjuk.

<>Copy
@NgModule({ providers: })

Így most már csak egy helyen kell módosítanunk a hibakezelés kódját.

Kliensoldali hibákLink erre a szakaszra

A kliensoldalon, ha valami váratlan történik, JavaScript hibát dobunk. Két fontos tulajdonsága van, amit használhatunk.

  1. message – A hiba ember által olvasható leírása.
  2. stack – A hiba veremkövetése az előzményekkel (call stack), hogy mely fájlok voltak “felelősek” a hiba előidézéséért.

Tipikusan a message tulajdonság az, amit a felhasználónak mutatunk, ha nem írjuk meg a hibaüzeneteket.

Szerveroldali hibákLink erre a szakaszra

A szerveroldalon, ha valami rosszul megy, egy HttpErrorResponse-t kapunk vissza. A JavaScript hibához hasonlóan ez is rendelkezik egy message tulajdonsággal, amelyet értesítésekhez használhatunk.

A hiba állapotkódját is visszaadja. Ezek különböző típusúak lehetnek. Ha négyessel kezdődik (4xx), akkor az ügyfél valami váratlant csinált. Ha például a 400-as státuszt kapjuk (Bad Request), akkor a kliens által küldött kérés nem az volt, amire a szerver számított.

A ötössel (5xx) kezdődő státuszok szerverhibák. A legtipikusabb az 500 Internal Server Error, egy nagyon általános HTTP státuszkód, amely azt jelenti, hogy valami elromlott a szerveren, de a szerver nem tudta pontosabban megmondani, hogy mi a pontos probléma.

A különböző típusú hibáknál hasznos egy olyan szolgáltatás, amely elemzi az üzeneteket és a stack traces-t belőlük.

Hiba szolgáltatásLink erre a szakaszra

Ezzel a szolgáltatással adjuk hozzá a logikát a szerver és a kliens hibaüzeneteinek és stack traces-einek elemzésére. Ez a példa nagyon leegyszerűsített. Fejlettebb felhasználási esetekhez használhatunk valami olyasmit, mint a stacktrace.js.

A logika ebben a szolgáltatásban attól függ, hogy milyen hibákat kapunk a backendtől. Attól is függ, hogy milyen üzenetet akarunk megjeleníteni a felhasználóinknak.

Általában nem mutatjuk meg a stack trace-t a felhasználóinknak. Ha azonban nem termelési környezetben vagyunk, előfordulhat, hogy a stack trace-t meg akarjuk mutatni a tesztelőknek. Ebben a forgatókönyvben beállíthatunk egy olyan zászlót, amely megmutatja a stack trace-t.

<>Copy
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 erre a szakaszra

A HttpInterceptor az Angular 4.3.1-gyel került bevezetésre. Lehetőséget biztosít a HTTP-kérések és -válaszok elfogására, hogy átalakítsuk vagy kezeljük őket, mielőtt továbbadnánk őket.

Két felhasználási esetet valósíthatunk meg az interceptorban.

Először is, egyszer vagy többször is újrapróbálhatjuk a HTTP-hívást, mielőtt hibát dobnánk. Bizonyos esetekben, például ha időtúllépést kapunk, folytathatjuk anélkül, hogy dobnánk a kivételt.

Ezért használjuk az RxJS retry operátorát, hogy újra feliratkozzunk a megfigyelhetőre.

További példák az ilyen viselkedésre:

  • Egy megfigyelhető szekvencia újratöltése hiba esetén egyéni kritériumok alapján
  • A RxJS ereje exponenciális backoff használata esetén

Ezután ellenőrizhetjük a kivétel állapotát, hogy 401 nem engedélyezett hibáról van-e szó. Token-alapú biztonság esetén megpróbálhatjuk frissíteni a tokent. Ha ez nem működik, akkor átirányíthatjuk a felhasználót a bejelentkezési oldalra.

<>Másolás
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); } }) ); }}

Itt egyszer újrapróbáljuk, mielőtt ellenőriznénk a hiba állapotát, és újra eldobnánk a hibát. A biztonsági tokenek frissítése nem tartozik ennek a cikknek a tárgykörébe.

Az általunk létrehozott interceptort is meg kell adnunk.

<>Copy
providers:

NotificationsLink erre a szakaszra

Az értesítésekhez az Angular Material Snackbar-t használom.

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

Ezzel egyszerű értesítéseket kap a felhasználó hiba esetén.

A szerveroldali és kliensoldali hibákat különbözőképpen tudjuk kezelni. Értesítések helyett megjeleníthetünk egy hibaoldalt.

HibaüzenetLink erre a szakaszra

A hibaüzenetek számítanak, és ezért kell, hogy legyen valamilyen jelentésük, hogy segítsék a felhasználót a továbblépésben. A “Hiba történt” megjelenítésével nem mondjuk el a felhasználónak, hogy mi a probléma, vagy hogyan oldja meg azt.

Ha ehhez képest, ha ehelyett valami olyasmit mutatunk, hogy “Sajnálom, elfogyott a pénze.”, akkor a felhasználó tudja, hogy mi a hiba. Kicsit jobb, de nem segít neki a hiba megoldásában.

Még jobb megoldás lenne, ha azt mondanánk, hogy utaljon még pénzt, és megadnánk egy linket egy pénzátutalási oldalra.

Ne feledjük, hogy a hibakezelés nem helyettesíti a rossz UX-et.

Azt értem ez alatt, hogy ne legyenek elvárt hibák. Ha a felhasználó megtehet valamit, ami hibát dob, akkor javítsd ki!

Ne engedj át egy hibát csak azért, mert egy szép hibaüzenetet készítettél hozzá.

NaplózásLink erre a szakaszra

Ha nem naplózzuk a hibákat, akkor csak az a felhasználó tud róluk, aki belefut. Az információk elmentése azért szükséges, hogy később meg tudjuk oldani a hiba elhárítását.

Ha már eldöntöttük, hogy tároljuk az adatokat, azt is meg kell választanunk, hogy hogyan mentjük el őket. Erről később bővebben.

Hová mentsük az adatokat?

A központosított hibakezeléssel nem kell túlságosan sajnálnunk, hogy későbbre hagyjuk a döntést. Már csak egy helyen kell módosítanunk a kódunkat. Egyelőre naplózzuk az üzenetet a konzolra.

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

HibakövetésLink erre a szakaszra

A webes alkalmazásunkban előforduló hibákat szeretnénk azonosítani, mielőtt a felhasználók találkoznának velük. A hibakövetés a problémák proaktív azonosításának és a lehető leggyorsabb javításának folyamata.

Azért nem dőlhetünk hátra, és nem várhatjuk el, hogy a felhasználók jelentsenek nekünk hibákat. Ehelyett proaktívnak kell lennünk a hibák naplózásával és nyomon követésével.

A hibákról akkor kell tudnunk, amikor azok bekövetkeznek.

Ezért létrehozhatjuk a megoldásunkat. Azonban miért találnánk fel újra a kereket, amikor annyi kiváló szolgáltatás létezik, mint például a Bugsnag, a Sentry, a TrackJs és a Rollbar, amelyek erre a területre specializálódtak.

Egy ilyen front-end hibakövetési megoldás használatával rögzíthetjük és visszajátszhatjuk a felhasználói munkameneteket, így a saját szemünkkel láthatjuk, hogy pontosan mit tapasztalt a felhasználó.

Ha nem tudsz reprodukálni egy hibát, akkor nem tudod kijavítani.

Más szóval, egy megfelelő hibakövetési megoldás figyelmeztethet, ha hiba lép fel, és betekintést nyújthat abba, hogyan lehet megismételni/megoldani a problémát.

Egy korábbi cikkben, a Hogyan küldjünk hibákat a Slackbe az Angularban, beszéltem a Slack használatáról a hibák követésére. Példaként itt is használhatnánk:

<>Másolás
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); }}

A robusztusabb megoldás megvalósítása nem tartozik ennek a cikknek a tárgykörébe.

All together nowLink erre a szakaszra

Mivel a hibakezelés alapvető fontosságú, ezért ez kerül először betöltésre. Emiatt nem tudunk függőségi injektálást használni a szolgáltatások konstruktorában. Ehelyett manuálisan kell injektálnunk őket az Injectorral.

<>Másolat
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); }}

KövetkeztetésLink erre a szakaszra

A hibakezelés egy vállalati alkalmazás sarokköve. Ebben a cikkben központosítottuk azt az ErrorHandler alapértelmezett viselkedésének felülírásával. Ezután számos szolgáltatást adtunk hozzá:

  1. Hiba szolgáltatás az üzenetek és a stack traces elemzésére.
  2. Értesítési szolgáltatás a felhasználók hibákról való értesítésére.
  3. Logging szolgáltatás a hibák naplózására.

Egy interceptor osztályt is implementáltunk:

  1. Hiba dobása előtt visszahívjuk.
  2. Egy bizonyos hibák ellenőrzése és megfelelő válaszadás.

Ezzel a megoldással elkezdheti a hibák követését, és remélhetőleg jobb élményt nyújt a felhasználóknak.

Példakód a GitHubon.
Futtassa a kódot a StackBlitz-en.

ForrásokLink erre a szakaszra

  • Hibakezelés & Angular by Aleix Suau
  • Hibák kezelése Javascriptben: The Definitive Guide by Lukas Gisder-Dubé
  • Global Error Handling with Angular 2+ by Austin
  • Angular applications – error handling and elegant recovery by Brachi Packter
  • The Art of the Error Message by Marina Posniak
  • JavaScript Weekly: Graceful Error Handling by Severin Perez

Diskurzus a közösséggel

Leave a Reply