Aspettarsi l’inaspettato – Migliori pratiche per la gestione degli errori in Angular
“Aspettarsi l’inaspettato dimostra un intelletto completamente moderno”. – Oscar Wilde
Questo articolo riguarda la centralizzazione della gestione degli errori in Angular. Discuto alcuni degli argomenti più comuni come:
- errori lato client
- errori lato server
- notifica all’utente
- errori di tracciamento
Presento alcuni frammenti di codice durante il percorso e infine fornisco un link all’esempio completo.
Versione spagnola:
- Esperando lo Inesperado – Buenas prácticas para el manejo de errores en Angular
A chi dobbiamo dare la colpa degli errori? Link a questa sezione
Perché abbiamo errori nelle nostre applicazioni? Perché non possiamo scrivere codice da specifiche che funzioni sempre?
In definitiva, gli esseri umani creano software, e siamo inclini a fare errori. Alcune ragioni dietro gli errori potrebbero essere:
- Complessità dell’applicazione
- Comunicazione tra le parti interessate
- Errori dello sviluppatore
- Pressione del tempo
- Mancanza di test
Questa lista potrebbe continuare a lungo. Con questo in mente, arriva il momento in cui accade l’inaspettato, e viene lanciato un errore.
Catturateli se poteteLink a questa sezione
Per catturare le eccezioni sincrone nel codice, possiamo aggiungere un blocco try/catch
. Se un errore viene lanciato all’interno di try
, allora lo catch
gestiamo. Se non lo facciamo, l’esecuzione dello script si ferma.
<>Copiatry { throw new Error('En error happened');}catch (error) { console.error('Log error', error);}console.log('Script execution continues');
Si capisce che questo diventa insostenibile molto velocemente. Non possiamo cercare di catturare gli errori ovunque nel codice. Abbiamo bisogno di una gestione globale degli errori.
Catch’em allLink a questa sezione
Fortunatamente, Angular fornisce un gancio per la gestione centralizzata delle eccezioni con ErrorHandler.
L’implementazione predefinita di
ErrorHandler
stampa messaggi di errore nelconsole
.
Possiamo modificare questo comportamento creando una classe che implementi l’ErrorHandler:
<>Copyimport { ErrorHandler } from '@angular/core';@Injectable()export class GlobalErrorHandler implements ErrorHandler { handleError(error) { // your custom error handling logic }}
Poi, lo forniamo nel nostro modulo root per cambiare il comportamento di default nella nostra applicazione. Invece di usare la classe ErrorHandler di default usiamo la nostra classe.
@NgModule({ providers: })
Così ora abbiamo solo un posto dove cambiare il codice per la gestione degli errori.
Errori lato clientLink a questa sezione
Sul lato client, quando succede qualcosa di inaspettato, viene lanciato un errore JavaScript. Ha due importanti proprietà che possiamo usare.
- messaggio – Descrizione leggibile dall’uomo dell’errore.
- stack – Stack trace dell’errore con una storia (call stack) di quali file sono stati ‘responsabili’ di causare quell’errore.
In genere, la proprietà message è ciò che mostriamo all’utente se non scriviamo i nostri messaggi di errore.
Erori lato serverLink a questa sezione
Sul lato server, quando qualcosa va storto, viene restituito un HttpErrorResponse. Come per l’errore JavaScript, ha una proprietà message che possiamo usare per le notifiche.
Ritorna anche il codice di stato dell’errore. Questo può essere di diversi tipi. Se inizia con un quattro (4xx), allora il client ha fatto qualcosa di inaspettato. Per esempio, se otteniamo lo stato 400 (Bad Request), allora la richiesta che il client ha inviato non era quella che il server si aspettava.
Gli stati che iniziano con cinque (5xx) sono errori del server. Il più tipico è il 500 Internal Server Error, un codice di stato HTTP molto generico che significa che qualcosa è andato storto sul server, ma il server non potrebbe essere più specifico su quale sia il problema esatto.
Con diversi tipi di errori, è utile un servizio che analizzi i messaggi e le tracce dello stack da essi.
Error serviceLink a questa sezione
In questo servizio, si aggiunge la logica per analizzare i messaggi di errore e le tracce dello stack dal server e dal client. Questo esempio è molto semplicistico. Per casi d’uso più avanzati potremmo usare qualcosa come stacktrace.js.
La logica in questo servizio dipende dal tipo di errori che riceviamo dal nostro backend. Dipende anche dal tipo di messaggio che vogliamo mostrare ai nostri utenti.
Di solito, non mostriamo lo stack trace ai nostri utenti. Tuttavia, se non siamo in un ambiente di produzione, potremmo voler mostrare lo stack trace ai tester. In questo scenario, possiamo impostare un flag che mostri lo stack trace.
<>Copyimport { 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 a questa sezione
HttpInterceptor è stato introdotto con Angular 4.3.1. Fornisce un modo per intercettare le richieste e le risposte HTTP per trasformarle o gestirle prima di passarle.
Ci sono due casi d’uso che possiamo implementare nell’intercettore.
In primo luogo, possiamo riprovare la chiamata HTTP una o più volte prima di lanciare l’errore. In alcuni casi, per esempio, se otteniamo un timeout, possiamo continuare senza lanciare l’eccezione.
Per questo, usiamo l’operatore retry di RxJS per risottoscrivere l’osservabile.
Esempi più avanzati di questo tipo di comportamento:
- Ritiriamo una sequenza osservabile su errore in base a criteri personalizzati
- Potere di RxJS quando si usa il backoff esponenziale
Possiamo quindi controllare lo stato dell’eccezione e vedere se è un errore 401 non autorizzato. Con la sicurezza basata sul token, possiamo provare ad aggiornare il token. Se questo non funziona, possiamo reindirizzare l’utente alla pagina di login.
<>Copiaimport { 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); } }) ); }}
Qui riproviamo una volta prima di controllare lo stato di errore e rilanciare l’errore. Il refresh dei token di sicurezza è fuori dallo scopo di questo articolo.
Dobbiamo anche fornire l’intercettore che abbiamo creato.
providers:
NotificheLink a questa sezione
Per le notifiche, sto usando Angular Material Snackbar.
<>Copiaimport { 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: }); }}
Con questo, abbiamo semplici notifiche per l’utente quando si verificano errori.
Possiamo gestire gli errori lato server e lato client in modo diverso. Invece delle notifiche, potremmo mostrare una pagina di errore.
Messaggio di erroreLink a questa sezione
I messaggi di errore sono importanti e dovrebbero, quindi, avere un qualche significato per aiutare l’utente ad andare avanti. Mostrando “Si è verificato un errore” non stiamo dicendo all’utente qual è il problema o come risolverlo.
In confronto, se invece mostriamo qualcosa come “Spiacente, sei senza soldi.” allora l’utente sa qual è l’errore. Un po’ meglio, ma non li aiuta a risolvere l’errore.
Una soluzione ancora migliore sarebbe quella di dire loro di trasferire più soldi e dare un link a una pagina di trasferimento di denaro.
Ricordate che la gestione degli errori non è un sostituto per una cattiva UX.
Quello che voglio dire con questo è che non dovreste avere nessun errore previsto. Se un utente può fare qualcosa che genera un errore, allora correggetelo!
Non lasciate passare un errore solo perché avete creato un bel messaggio di errore per esso.
LoggingLink a questa sezione
Se non registriamo gli errori, allora solo l’utente che li incontra li conosce. Salvare le informazioni è necessario per essere in grado di risolvere il problema in seguito.
Quando abbiamo deciso di memorizzare i dati dobbiamo anche scegliere come salvarli. Più avanti.
Dove dovremmo salvare i dati?
Con la gestione centralizzata degli errori, non dobbiamo dispiacerci troppo per aver lasciato la decisione per dopo. Abbiamo solo un posto dove cambiare il nostro codice ora. Per ora, registriamo il messaggio nella console.
<>Copiaimport { 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 a questa sezione
In definitiva, vuoi identificare i bug nella tua applicazione web prima che gli utenti li incontrino. L’error tracking è il processo di identificazione proattiva dei problemi e la loro correzione il più velocemente possibile.
Quindi, non possiamo semplicemente sederci e aspettare che gli utenti ci segnalino gli errori. Invece, dovremmo essere proattivi registrando e monitorando gli errori.
Dovremmo sapere degli errori quando accadono.
Potremmo creare la nostra soluzione per questo scopo. Tuttavia, perché reinventare la ruota quando ci sono così tanti servizi eccellenti come Bugsnag, Sentry, TrackJs e Rollbar specializzati in questo settore.
Utilizzare una di queste soluzioni di tracciamento degli errori front-end può permettervi di registrare e riprodurre le sessioni degli utenti in modo da poter vedere da soli esattamente ciò che l’utente ha sperimentato.
Se non puoi riprodurre un bug, allora non puoi risolverlo.
In altre parole, una corretta soluzione di error tracking potrebbe avvisarti quando si verifica un errore e fornire indicazioni su come replicare/risolvere il problema.
In un precedente articolo, Come inviare errori in Slack in Angular ho parlato dell’utilizzo di Slack per tracciare gli errori. Come esempio, potremmo usarlo qui:
<>Copiaimport { 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); }}
Implementare una soluzione più robusta è fuori dallo scopo di questo articolo.
Tutti insieme oraLink a questa sezione
Siccome la gestione degli errori è essenziale, viene caricata per prima. A causa di questo, non possiamo usare l’iniezione di dipendenza nel costruttore per i servizi. Invece, dobbiamo iniettarli manualmente con 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); }}
ConclusioneLink a questa sezione
La gestione degli errori è una pietra miliare per un’applicazione aziendale. In questo articolo, l’abbiamo centralizzata sovrascrivendo il comportamento predefinito di ErrorHandler. Abbiamo poi aggiunto diversi servizi:
- Servizio di errore per analizzare i messaggi e le tracce dello stack.
- Servizio di notifica per avvisare gli utenti degli errori.
- Servizio di log per registrare gli errori.
Abbiamo anche implementato una classe intercettatrice per:
- Rispondere prima di lanciare l’errore.
- Verificare gli errori specifici e rispondere di conseguenza.
Con questa soluzione, puoi iniziare a tracciare i tuoi errori e sperare di dare agli utenti un’esperienza migliore.
Codice di esempio su GitHub.
Esegui il codice su StackBlitz.
RisorseLink a questa sezione
- Gestione degli errori & Angular di Aleix Suau
- Gestione degli errori in Javascript: The Definitive Guide di Lukas Gisder-Dubé
- Global Error Handling with Angular 2+ di Austin
- Applicazioni Angular – gestione degli errori e recupero elegante di Brachi Packter
- The Art of the Error Message di Marina Posniak
- JavaScript Weekly: Graceful Error Handling di Severin Perez
Discuti con la comunità
Leave a Reply