Esperando lo inesperado – Mejores prácticas para el manejo de errores en Angular

«Esperar lo inesperado muestra un intelecto completamente moderno». – Oscar Wilde

Este artículo trata sobre la centralización del manejo de errores en Angular. Discuto algunos de los temas más comunes como:

  • errores del lado del cliente
  • errores del lado del servidor
  • notificación al usuario
  • seguimiento de errores

Presento algunos fragmentos de código durante el camino y por último proporciono un enlace al ejemplo completo.

Versión en español:

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

¿A quién debemos culpar de los errores?

¿Por qué tenemos errores en nuestras aplicaciones? Por qué no podemos escribir código a partir de especificaciones que siempre funcione?

En definitiva, los seres humanos creamos software, y somos propensos a cometer errores. Algunas razones detrás de los errores podrían ser:

  1. Complejidad de la aplicación
  2. Comunicación entre las partes interesadas
  3. Errores del desarrollador
  4. Presión de tiempo
  5. Falta de pruebas

Esta lista podría seguir y seguir. Teniendo esto en cuenta, llega el momento en el que sucede lo inesperado, y se lanza un error.

Cógelos si puedesEnlace a esta sección

Para atrapar excepciones sincrónicas en el código, podemos añadir un bloque try/catch. Si se lanza un error dentro de try entonces lo catchmanejamos. Si no lo hacemos, la ejecución del script se detiene.

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

Entendiblemente, esto se vuelve insostenible muy rápido. No podemos tratar de atrapar los errores en todas partes en el código. Necesitamos un manejo de errores global.

Catch’em allEnlace a esta sección

Afortunadamente, Angular proporciona un gancho para el manejo de excepciones centralizado con ErrorHandler.

La implementación por defecto de ErrorHandler imprime mensajes de error en el console.

Podemos modificar este comportamiento creando una clase que implemente el ErrorHandler:

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

Entonces, lo proporcionamos en nuestro módulo raíz para cambiar el comportamiento por defecto en nuestra aplicación. En lugar de utilizar la clase ErrorHandler por defecto estamos utilizando nuestra clase.

<>Copiar
@NgModule({ providers: })

Así que ahora sólo tenemos un lugar donde cambiar el código para el manejo de errores.

Errores del lado del clienteEnlace a esta sección

En el lado del cliente, cuando sucede algo inesperado, se lanza un Error JavaScript. Tiene dos propiedades importantes que podemos utilizar.

  1. Mensaje – Descripción legible por humanos del error.
  2. Pila – Rastreo de la pila del error con un historial (pila de llamadas) de los archivos que fueron ‘responsables’ de causar ese Error.

Típicamente, la propiedad message es lo que mostramos al usuario si no escribimos nuestros mensajes de error.

Errores del lado del servidorEnlace a esta sección

En el lado del servidor, cuando algo va mal, se devuelve un HttpErrorResponse. Al igual que el error de JavaScript, tiene una propiedad message que podemos utilizar para las notificaciones.

También devuelve el código de estado del error. Este puede ser de diferentes tipos. Si empieza por un cuatro (4xx), es que el cliente ha hecho algo inesperado. Por ejemplo, si obtenemos el estado 400 (Bad Request), entonces la petición que el cliente envió no era lo que el servidor esperaba.

Los estados que empiezan por cinco (5xx) son errores del servidor. El más típico es el 500 Internal Server Error, un código de estado HTTP muy general que significa que algo ha ido mal en el servidor, pero el servidor no ha podido ser más específico sobre cuál es el problema exacto.

Con diferentes tipos de errores, es útil con un servicio que analice los mensajes y las trazas de pila de los mismos.

Servicio de erroresEnlace a esta sección

En este servicio, añadimos la lógica para analizar los mensajes de error y las trazas de pila del servidor y del cliente. Este ejemplo es muy simplista. Para casos de uso más avanzados podríamos utilizar algo como stacktrace.js.

La lógica de este servicio depende de qué tipo de errores recibimos de nuestro backend. También depende del tipo de mensaje que queramos mostrar a nuestros usuarios.

Normalmente, no mostramos el stacktrace a nuestros usuarios. Sin embargo, si no estamos en un entorno de producción, es posible que queramos mostrar el stack trace a los probadores. En ese escenario, podemos establecer una bandera que muestre el stack trace.

<>Copia
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 a esta sección

HttpInterceptor fue introducido con Angular 4.3.1. Proporciona una forma de interceptar las peticiones y respuestas HTTP para transformarlas o manejarlas antes de pasarlas.

Hay dos casos de uso que podemos implementar en el interceptor.

Primero, podemos reintentar la llamada HTTP una o varias veces antes de lanzar el error. En algunos casos, por ejemplo, si obtenemos un timeout, podemos continuar sin lanzar la excepción.

Para ello, utilizamos el operador retry de RxJS para volver a suscribirnos al observable.

Ejemplos más avanzados de este tipo de comportamiento:

  • Reintentar una secuencia observable en caso de error basándose en criterios personalizados
  • Potencia de RxJS al usar backoff exponencial

Podemos entonces comprobar el estado de la excepción y ver si es un error 401 no autorizado. Con la seguridad basada en tokens, podemos intentar refrescar el token. Si esto no funciona, podemos redirigir al usuario a la página de inicio de sesión.

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

Aquí reintentamos una vez antes de comprobar el estado del error y volver a lanzarlo. Refrescar los tokens de seguridad está fuera del alcance de este artículo.

También necesitamos proporcionar el interceptor que creamos.

<>Copiar
providers:

NotificacionesEnlace a esta sección

Para las notificaciones, estoy usando Angular Material Snackbar.

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

Con esto, tenemos notificaciones simples para el usuario cuando se producen errores.

Podemos manejar los errores del lado del servidor y del lado del cliente de manera diferente. En lugar de notificaciones, podríamos mostrar una página de error.

Mensaje de errorEnlace a esta sección

Los mensajes de error importan y deben, por tanto, tener algún significado para ayudar al usuario a seguir adelante. Al mostrar «Se ha producido un error» no estamos diciendo al usuario cuál es el problema o cómo resolverlo.

En comparación, si en su lugar mostramos algo como «Lo sentimos, se ha quedado sin dinero.» entonces el usuario sabe cuál es el error. Un poco mejor, pero no les ayuda a resolver el error.

Una solución aún mejor sería decirles que transfieran más dinero y dar un enlace a una página de transferencia de dinero.

Recuerda que el manejo de errores no es un sustituto de una mala UX.

Lo que quiero decir con esto es que no deberías tener ningún error esperado. Si un usuario puede hacer algo que arroja un error, entonces arréglalo.

No dejes pasar un error sólo porque has creado un bonito mensaje de error para él.

LoggingEnlace a esta sección

Si no registramos los errores, entonces sólo el usuario que se encuentra con ellos los conoce. Guardar la información es necesario para poder solucionar el problema más adelante.

Cuando hayamos decidido almacenar los datos también tenemos que elegir cómo guardarlos. Más adelante hablaremos de ello.

¿Dónde debemos guardar los datos?

Con la gestión de errores centralizada, no tenemos que lamentarnos demasiado por dejar la decisión para más adelante. Ahora sólo tenemos un lugar para cambiar nuestro código. Por ahora, vamos a registrar el mensaje en la consola.

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

Seguimiento de erroresEnlace a esta sección

En realidad, quieres identificar los errores en tu aplicación web antes de que los usuarios los encuentren. El seguimiento de errores es el proceso de identificar proactivamente los problemas y solucionarlos lo antes posible.

Así que no podemos quedarnos sentados y esperar que los usuarios nos informen de los errores. En su lugar, debemos ser proactivos registrando y monitorizando los errores.

Debemos conocer los errores cuando se producen.

Podríamos crear nuestra solución para este fin. Sin embargo, por qué reinventar la rueda cuando hay tantos servicios excelentes como Bugsnag, Sentry, TrackJs y Rollbar que se especializan en esta área.

Usar una de estas soluciones de seguimiento de errores de front-end puede permitirle grabar y reproducir las sesiones de los usuarios para que pueda ver por sí mismo exactamente lo que el usuario experimentó.

Si no puedes reproducir un error, entonces no puedes solucionarlo.

En otras palabras, una solución de seguimiento de errores adecuada podría alertarte cuando se produce un error y proporcionar información sobre cómo replicar/resolver el problema.

En un artículo anterior, Cómo enviar errores a Slack en Angular hablé sobre el uso de Slack para rastrear errores. Como ejemplo, podríamos usarlo aquí:

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

Implementar una solución más robusta está fuera del alcance de este artículo.

Todo junto ahoraEnlace a esta sección

Como el manejo de errores es esencial, se carga primero. Debido a esto, no podemos utilizar la inyección de dependencia en el constructor para los servicios. En su lugar, tenemos que inyectarlos manualmente con Injector.

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

ConclusiónEnlace a esta sección

El manejo de errores es una piedra angular para una aplicación empresarial. En este artículo, lo centralizamos anulando el comportamiento por defecto de ErrorHandler. A continuación, añadimos varios servicios:

  1. Servicio de errores para analizar los mensajes y las trazas de pila.
  2. Servicio de notificaciones para notificar a los usuarios sobre los errores.
  3. Servicio de registro para registrar los errores.

También implementamos una clase interceptora para:

  1. Recordar antes de lanzar el error.
  2. Comprobar errores específicos y responder en consecuencia.

Con esta solución, puedes empezar a rastrear tus errores y, con suerte, dar a los usuarios una mejor experiencia.

Código de ejemplo en GitHub.
Ejecuta el código en StackBlitz.

RecursosEnlace a esta sección

  • Manejo de errores & Angular por Aleix Suau
  • Manejo de errores en Javascript: The Definitive Guide por Lukas Gisder-Dubé
  • Global Error Handling with Angular 2+ por Austin
  • Angular applications – error handling and elegant recovery por Brachi Packter
  • The Art of the Error Message por Marina Posniak
  • JavaScript Weekly: Graceful Error Handling por Severin Perez

Discute con la comunidad

Leave a Reply