S’attendre à l’inattendu – Meilleures pratiques pour la gestion des erreurs dans Angular

« S’attendre à l’inattendu montre un intellect tout à fait moderne. » – Oscar Wilde

Cet article porte sur la centralisation de la gestion des erreurs dans Angular. J’aborde certains des sujets les plus courants tels que :

  • les erreurs côté client
  • les erreurs côté serveur
  • la notification utilisateur
  • les erreurs de suivi

Je présente quelques extraits de code en cours de route et enfin je fournis un lien vers l’exemple complet.

Version espagnole:

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

Qui devons-nous blâmer pour les erreurs ? Lien vers cette section

Pourquoi avons-nous des erreurs dans nos applications ? Pourquoi ne pouvons-nous pas écrire du code à partir de spécifications qui fonctionne toujours ?

En fin de compte, les êtres humains créent des logiciels, et nous sommes enclins à faire des erreurs. Certaines raisons derrière les erreurs pourraient être :

  1. Complexité de l’application
  2. Communication entre les parties prenantes
  3. Erreurs du développeur
  4. Pression de temps
  5. Manque de tests

Cette liste pourrait continuer encore et encore. En gardant cela à l’esprit, le moment arrive où l’inattendu se produit, et une erreur est lancée.

Les attraper si vous le pouvezLien à cette section

Pour attraper les exceptions synchrones dans le code, nous pouvons ajouter un bloc try/catch. Si une erreur est lancée à l’intérieur de try, alors nous catchla gérons. Si nous ne le faisons pas, l’exécution du script s’arrête.

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

Intéressant, cela devient très vite insoutenable. Nous ne pouvons pas essayer d’attraper les erreurs partout dans le code. Nous avons besoin d’une gestion globale des erreurs.

Catch’em allLien vers cette section

Heureusement, Angular fournit un crochet pour la gestion centralisée des exceptions avec ErrorHandler.

L’implémentation par défaut de ErrorHandler imprime les messages d’erreur dans le console.

Nous pouvons modifier ce comportement en créant une classe qui implémente le ErrorHandler:

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

Puis, nous la fournissons dans notre module racine pour changer le comportement par défaut dans notre application. Au lieu d’utiliser la classe ErrorHandler par défaut, nous utilisons notre classe.

<>Copy
@NgModule({ providers: })

Donc maintenant, nous n’avons qu’un seul endroit où modifier le code pour la gestion des erreurs.

Erreurs côté clientLien vers cette section

Côté client, lorsque quelque chose d’inattendu se produit, une erreur JavaScript est lancée. Elle possède deux propriétés importantes que nous pouvons utiliser.

  1. message – Description lisible par l’homme de l’erreur.
  2. stack – Trace de la pile d’erreurs avec un historique (pile d’appels) des fichiers qui étaient « responsables » de la cause de cette erreur.

Typiquement, la propriété message est ce que nous montrons à l’utilisateur si nous n’écrivons pas nos messages d’erreur.

Erreurs côté serveurLien vers cette section

Côté serveur, lorsque quelque chose ne va pas, une HttpErrorResponse est renvoyée. Comme pour l’erreur JavaScript, elle possède une propriété message que nous pouvons utiliser pour les notifications.

Elle renvoie également le code d’état de l’erreur. Celui-ci peut être de différents types. S’il commence par un quatre (4xx), alors le client a fait quelque chose d’inattendu. Par exemple, si nous obtenons le statut 400 (Bad Request), alors la requête que le client a envoyée n’était pas ce que le serveur attendait.

Les statuts commençant par cinq (5xx) sont des erreurs de serveur. Le plus typique est l’erreur 500 Internal Server Error, un code de statut HTTP très général qui signifie que quelque chose a mal tourné sur le serveur, mais le serveur n’a pas pu être plus précis sur le problème exact.

Avec différents types d’erreurs, il est utile avec un service qui analyse les messages et les traces de pile de ceux-ci.

Service d’erreurLien vers cette section

Dans ce service, nous ajoutons la logique pour analyser les messages d’erreur et les traces de pile du serveur et du client. Cet exemple est très simpliste. Pour des cas d’utilisation plus avancés, nous pourrions utiliser quelque chose comme stacktrace.js.

La logique dans ce service dépend du type d’erreurs que nous recevons de notre backend. Elle dépend également du type de message que nous voulons montrer à nos utilisateurs.

En général, nous ne montrons pas la trace de pile à nos utilisateurs. Cependant, si nous ne sommes pas dans un environnement de production, nous pourrions vouloir montrer la trace de pile aux testeurs. Dans ce scénario, nous pouvons définir un drapeau qui montre la trace de la pile.

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

HttpInterceptorLien vers cette section

HttpInterceptor a été introduit avec Angular 4.3.1. Il fournit un moyen d’intercepter les demandes et les réponses HTTP pour les transformer ou les traiter avant de les transmettre.

Il y a deux cas d’utilisation que nous pouvons mettre en œuvre dans l’intercepteur.

Premièrement, nous pouvons réessayer l’appel HTTP une ou plusieurs fois avant de lancer l’erreur. Dans certains cas, par exemple, si nous obtenons un timeout, nous pouvons continuer sans lancer l’exception.

Pour cela, nous utilisons l’opérateur retry de RxJS pour nous réabonner à l’observable.

Des exemples plus avancés de ce genre de comportement:

  • Réessayer une séquence observable sur erreur basée sur des critères personnalisés
  • Pouvoir de RxJS lors de l’utilisation du backoff exponentiel

Nous pouvons alors vérifier le statut de l’exception et voir si c’est une erreur 401 non autorisée. Avec la sécurité basée sur les jetons, nous pouvons essayer de rafraîchir le jeton. Si cela ne fonctionne pas, nous pouvons rediriger l’utilisateur vers la page de connexion.

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

Ici nous réessayons une fois avant de vérifier le statut de l’erreur et de relancer l’erreur. Le rafraîchissement des jetons de sécurité sort du cadre de cet article.

Nous devons également fournir l’intercepteur que nous avons créé.

<>Copy
providers:

NotificationsLien vers cette section

Pour les notifications, j’utilise Angular Material Snackbar.

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

Avec cela, nous avons des notifications simples pour l’utilisateur lorsque des erreurs se produisent.

Nous pouvons gérer les erreurs côté serveur et côté client différemment. Au lieu de notifications, nous pourrions montrer une page d’erreur.

Message d’erreurLien vers cette section

Les messages d’erreur ont de l’importance et devraient, par conséquent, avoir une certaine signification pour aider l’utilisateur à avancer. En affichant « Une erreur est survenue », nous ne disons pas à l’utilisateur quel est le problème ou comment le résoudre.

En comparaison, si nous affichons plutôt quelque chose comme « Désolé, vous n’avez plus d’argent. », l’utilisateur sait alors quelle est l’erreur. Un peu mieux mais cela ne les aide pas à résoudre l’erreur.

Une solution encore meilleure serait de leur dire de transférer plus d’argent et de donner un lien vers une page de transfert d’argent.

N’oubliez pas que la gestion des erreurs ne remplace pas une mauvaise UX.

Ce que je veux dire par là, c’est que vous ne devriez pas avoir d’erreurs attendues. Si un utilisateur peut faire quelque chose qui jette une erreur, alors réparez-le !

Ne laissez pas passer une erreur juste parce que vous avez créé un beau message d’erreur pour elle.

LoggingLien vers cette section

Si nous ne loggeons pas les erreurs, alors seul l’utilisateur qui les rencontre en est informé. Sauvegarder l’information est nécessaire pour pouvoir dépanner le problème plus tard.

Quand nous avons décidé de stocker les données, nous devons aussi choisir comment les sauvegarder. Nous y reviendrons plus tard.

Où devrions-nous enregistrer les données ?

Avec la gestion centralisée des erreurs, nous n’avons pas à nous sentir trop désolés de laisser la décision pour plus tard. Nous n’avons qu’un seul endroit pour modifier notre code maintenant. Pour l’instant, enregistrons le message dans la console.

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

Traçage des erreursLien vers cette section

Idéalement, vous voulez identifier les bogues dans votre application Web avant que les utilisateurs ne les rencontrent. Le suivi des erreurs est le processus d’identification proactive des problèmes et de leur correction aussi rapidement que possible.

Donc, nous ne pouvons pas rester assis et attendre que les utilisateurs nous signalent les erreurs. Au lieu de cela, nous devrions être proactifs en enregistrant et en surveillant les erreurs.

Nous devrions connaître les erreurs quand elles se produisent.

Nous pourrions créer notre solution à cette fin. Cependant, pourquoi réinventer la roue quand il y a tant d’excellents services comme Bugsnag, Sentry, TrackJs et Rollbar spécialisés dans ce domaine.

L’utilisation d’une de ces solutions frontales de suivi des erreurs peut vous permettre d’enregistrer et de rejouer les sessions des utilisateurs afin que vous puissiez voir par vous-même exactement ce que l’utilisateur a vécu.

Si vous ne pouvez pas reproduire un bug, alors vous ne pouvez pas le réparer.

En d’autres termes, une solution de suivi des erreurs appropriée pourrait vous alerter lorsqu’une erreur se produit et fournir des indications sur la façon de reproduire/résoudre le problème.

Dans un article précédent, Comment envoyer des erreurs dans Slack en Angular, j’ai parlé de l’utilisation de Slack pour suivre les erreurs. À titre d’exemple, nous pourrions l’utiliser ici :

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

La mise en œuvre d’une solution plus robuste sort du cadre de cet article.

Tout ensemble maintenantLien vers cette section

Puisque la gestion des erreurs est essentielle, elle est chargée en premier. Pour cette raison, nous ne pouvons pas utiliser l’injection de dépendance dans le constructeur pour les services. Au lieu de cela, nous devons les injecter manuellement avec Injector.

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

ConclusionLien vers cette section

La gestion des erreurs est une pierre angulaire pour une application d’entreprise. Dans cet article, nous l’avons centralisé en surchargeant le comportement par défaut de ErrorHandler. Nous avons ensuite ajouté plusieurs services :

  1. Service d’erreur pour analyser les messages et les traces de pile.
  2. Service de notification pour informer les utilisateurs des erreurs.
  3. Service de journalisation pour enregistrer les erreurs.

Nous avons également implémenté une classe d’interception pour :

  1. Réagir avant de lancer l’erreur.
  2. Vérifier les erreurs spécifiques et répondre en conséquence.

Avec cette solution, vous pouvez commencer à suivre vos erreurs et, espérons-le, donner aux utilisateurs une meilleure expérience.

Code exemple sur GitHub.
Exécuter le code sur StackBlitz.

RessourcesLien vers cette section

  • Gestion des erreurs & Angular par Aleix Suau
  • Gestion des erreurs en Javascript : The Definitive Guide par Lukas Gisder-Dubé
  • Gestion globale des erreurs avec Angular 2+ par Austin
  • Applications Angular – gestion des erreurs et récupération élégante par Brachi Packter
  • L’art du message d’erreur par Marina Posniak
  • JavaScript Weekly : Graceful Error Handling par Severin Perez

Discuter avec la communauté.

Leave a Reply