Expecting the Unexpected – Cele mai bune practici de tratare a erorilor în Angular
„Așteptarea neașteptatului arată un intelect complet modern.” – Oscar Wilde
Acest articol se referă la centralizarea gestionării erorilor în Angular. Discut unele dintre cele mai comune subiecte, cum ar fi:
- erorile de pe partea clientului
- erorile de pe partea serverului
- notificarea utilizatorului
- erorile de urmărire
Prezint câteva fragmente de cod pe parcurs și, în cele din urmă, ofer un link către exemplul complet.
Versiunea în limba spaniolă:
- Esperando lo Inesperado – Buenas practicas para el manejo de errores en Angular
Cu cine ar trebui să dăm vina pentru erori?Link către această secțiune
De ce avem erori în aplicațiile noastre? De ce nu putem scrie cod din specificații care să funcționeze întotdeauna?
În cele din urmă, ființele umane creează software, iar noi suntem predispuși să facem greșeli. Unele dintre motivele care stau la baza erorilor ar putea fi:
- Complexitatea aplicației
- Comunicarea între părțile interesate
- Erorile dezvoltatorului
- Presiunea timpului
- Lipsa testării
Această listă ar putea continua la nesfârșit. Ținând cont de acest lucru, vine momentul în care se întâmplă ceva neașteptat și se aruncă o eroare.
Capturează-le dacă poțiLegăturați această secțiune
Pentru a prinde excepțiile sincrone în cod, putem adăuga un bloc try/catch
. Dacă o eroare este aruncată în interiorul try
, atunci o catch
și o tratăm. Dacă nu facem acest lucru, execuția scriptului se oprește.
try { throw new Error('En error happened');}catch (error) { console.error('Log error', error);}console.log('Script execution continues');
În mod normal, acest lucru devine foarte repede nesustenabil. Nu putem încerca să prindem erorile peste tot în cod. Avem nevoie de gestionarea globală a erorilor.
Catch’em allLink to this section
Din fericire, Angular oferă un cârlig pentru gestionarea centralizată a excepțiilor cu ErrorHandler.
Implementarea implicită a
ErrorHandler
imprimă mesajele de eroare înconsole
.
Putem modifica acest comportament prin crearea unei clase care implementează ErrorHandler:
<>Copieimport { ErrorHandler } from '@angular/core';@Injectable()export class GlobalErrorHandler implements ErrorHandler { handleError(error) { // your custom error handling logic }}
Apoi, o furnizăm în modulul nostru rădăcină pentru a schimba comportamentul implicit în aplicația noastră. În loc să folosim clasa implicită ErrorHandler, folosim clasa noastră.
<>Copy@NgModule({ providers: })
Acum avem un singur loc în care să modificăm codul pentru gestionarea erorilor.
Erorile de pe partea clientuluiLink to this section
Pe partea clientului, când se întâmplă ceva neașteptat, se aruncă o eroare JavaScript. Aceasta are două proprietăți importante pe care le putem folosi.
- message – Descrierea lizibilă de către om a erorii.
- stack – Urmărirea stivei de erori cu un istoric (stivă de apeluri) a fișierelor care au fost „responsabile” de producerea acelei erori.
În mod obișnuit, proprietatea message este ceea ce arătăm utilizatorului dacă nu ne scriem mesajele de eroare.
Erorile de pe serverLink to this section
Pe partea serverului, atunci când ceva nu merge bine, se returnează un HttpErrorResponse. Ca și în cazul erorii JavaScript, acesta are o proprietate message pe care o putem folosi pentru notificări.
Se returnează, de asemenea, codul de stare al erorii. Acestea pot fi de diferite tipuri. Dacă începe cu un patru (4xx), atunci clientul a făcut ceva neașteptat. De exemplu, dacă primim statusul 400 (Bad Request), atunci cererea pe care clientul a trimis-o nu a fost ceea ce aștepta serverul.
Statele care încep cu cinci (5xx) sunt erori de server. Cel mai tipic este 500 Internal Server Error (Eroare internă a serverului), un cod de stare HTTP foarte general care înseamnă că ceva nu a mers bine pe server, dar serverul nu a putut fi mai specific cu privire la care este problema exactă.
Cu diferite tipuri de erori, este util un serviciu care analizează mesajele și urmele de stivă din acestea.
Serviciu de eroriLink to this section
În acest serviciu, adăugăm logica pentru analiza mesajelor de eroare și a urmelor de stivă de la server și client. Acest exemplu este foarte simplist. Pentru cazuri de utilizare mai avansate am putea folosi ceva de genul stacktrace.js.
Logica din acest serviciu depinde de tipul de erori pe care le primim de la backend-ul nostru. Depinde, de asemenea, de ce fel de mesaj dorim să le arătăm utilizatorilor noștri.
De obicei, nu arătăm stack trace-ul utilizatorilor noștri. Cu toate acestea, dacă nu ne aflăm într-un mediu de producție, am putea dori să arătăm stack trace-ul pentru testeri. În acest scenariu, putem seta un indicator care să arate stack trace.
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 către această secțiune
HttpInterceptor a fost introdus cu Angular 4.3.1. Acesta oferă o modalitate de a intercepta cererile și răspunsurile HTTP pentru a le transforma sau a le trata înainte de a le transmite mai departe.
Există două cazuri de utilizare pe care le putem implementa în interceptor.
Primul, putem încerca din nou apelul HTTP o dată sau de mai multe ori înainte de a arunca eroarea. În unele cazuri, de exemplu, dacă primim un timeout, putem continua fără a arunca excepția.
Pentru aceasta, folosim operatorul retry din RxJS pentru a ne resubscrie la observabil.
Exemple mai avansate ale acestui tip de comportament:
- Retry an observable sequence on error based on custom criteria
- Puterea lui RxJS atunci când folosește backoff exponențial
Puterea lui RxJS atunci când folosește backoff exponențial
Potem apoi să verificăm starea excepției și să vedem dacă este o eroare 401 neautorizată. Cu securitatea bazată pe token-uri, putem încerca să reîmprospătăm token-ul. Dacă acest lucru nu funcționează, putem redirecționa utilizatorul către pagina de autentificare.
<>Copieimport { 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); } }) ); }}
Aici încercăm din nou o dată înainte de a verifica starea erorii și de a retrimite eroarea. Reîmprospătarea token-urilor de securitate este în afara domeniului de aplicare al acestui articol.
De asemenea, trebuie să furnizăm interceptorul pe care l-am creat.
providers:
NotificațiiLink către această secțiune
Pentru notificări, folosesc Angular Material Snackbar.
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: }); }}
Cu aceasta, avem notificări simple pentru utilizator atunci când apar erori.
Pot fi gestionate în mod diferit erorile de pe partea serverului și cele de pe partea clientului. În loc de notificări, am putea afișa o pagină de eroare.
Mesaj de eroareLink către această secțiune
Mesajele de eroare contează și, prin urmare, ar trebui să aibă o anumită semnificație pentru a ajuta utilizatorul să avanseze. Arătând „A apărut o eroare” nu îi spunem utilizatorului care este problema sau cum să o rezolve.
În comparație, dacă în schimb arătăm ceva de genul „Ne pare rău, nu mai aveți bani.”, atunci utilizatorul știe care este eroarea. Un pic mai bine, dar nu îi ajută să rezolve eroarea.
O soluție și mai bună ar fi să le spunem să transfere mai mulți bani și să dăm un link către o pagină de transfer de bani.
Rețineți că gestionarea erorilor nu este un substitut pentru un UX prost.
Ce vreau să spun prin aceasta este că nu ar trebui să aveți erori așteptate. Dacă un utilizator poate face ceva care aruncă o eroare, atunci reparați-o!
Nu lăsați o eroare să treacă doar pentru că ați creat un mesaj de eroare frumos pentru ea.
LoggingLink către această secțiune
Dacă nu înregistrăm erorile, atunci doar utilizatorul care se întâlnește cu ele știe despre ele. Salvarea informațiilor este necesară pentru a putea depana problema mai târziu.
După ce am decis să stocăm datele, trebuie să alegem și modul în care să le salvăm. Mai multe despre asta mai târziu.
Unde ar trebui să salvăm datele?
Cu gestionarea centralizată a erorilor, nu trebuie să ne pară prea rău că am lăsat decizia pentru mai târziu. Acum avem un singur loc în care să ne modificăm codul. Deocamdată, să înregistrăm mesajul în consolă.
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); }}
Error TrackingLink la această secțiune
În mod normal, doriți să identificați erorile din aplicația dvs. web înainte ca utilizatorii să le întâlnească. Urmărirea erorilor este procesul de identificare proactivă a problemelor și de remediere a acestora cât mai repede posibil.
Atunci, nu putem sta degeaba și să ne așteptăm ca utilizatorii să ne raporteze erorile. În schimb, ar trebui să fim proactivi prin înregistrarea și monitorizarea erorilor.
Ar trebui să știm despre erori atunci când acestea se întâmplă.
Am putea crea soluția noastră în acest scop. Cu toate acestea, de ce să reinventăm roata când există atât de multe servicii excelente, cum ar fi Bugsnag, Sentry, TrackJs și Rollbar, specializate în acest domeniu.
Utilizarea uneia dintre aceste soluții front-end de urmărire a erorilor vă poate permite să înregistrați și să redați sesiunile utilizatorilor, astfel încât să puteți vedea cu ochii voștri ce a experimentat exact utilizatorul.
Dacă nu puteți reproduce o eroare, atunci nu o puteți rezolva.
Cu alte cuvinte, o soluție adecvată de urmărire a erorilor ar putea să vă avertizeze atunci când apare o eroare și să vă ofere informații despre cum să reproduceți/rezolvați problema.
Într-un articol anterior, How to send Errors into Slack in Angular am vorbit despre utilizarea Slack pentru a urmări erorile. Ca un exemplu, am putea să o folosim aici:
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); }}
Implementarea unei soluții mai robuste este în afara scopului acestui articol.
Toate împreună acumLegăturați această secțiune
Din moment ce gestionarea erorilor este esențială, aceasta se încarcă prima. Din acest motiv, nu putem folosi injecția de dependență în constructorul pentru servicii. În schimb, trebuie să le injectăm manual cu Injector.
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); }}
ConcluzieLink to this section
Manipularea erorilor este o piatră de temelie pentru o aplicație de întreprindere. În acest articol, am centralizat-o prin suprascrierea comportamentului implicit al ErrorHandler. Am adăugat apoi mai multe servicii:
- Serviciu de eroare pentru a analiza mesajele și urmele din stivă.
- Serviciu de notificare pentru a anunța utilizatorii despre erori.
- Serviciu de logare pentru a înregistra erorile.
Am implementat, de asemenea, o clasă de interceptare pentru:
- Retrimitere înainte de a arunca eroarea.
- Verificarea erorilor specifice și răspunsul corespunzător.
Cu această soluție, puteți începe să urmăriți erorile și, sperăm, să oferiți utilizatorilor o experiență mai bună.
Cod de exemplu pe GitHub.
Executați codul pe StackBlitz.
ResurseLink la această secțiune
- Error Handling & Angular de Aleix Suau
- Handling Errors in Javascript: The Definitive Guide de Lukas Gisder-Dubé
- Global Error Handling with Angular 2+ de Austin
- Angular applications – error handling and elegant recovery de Brachi Packter
- The Art of the Error Message de Marina Posniak
- JavaScript Weekly: Graceful Error Handling de Severin Perez
Discută cu comunitatea
Leave a Reply