¿Qué aprenderá?

En este tutorial podrá seguir los pasos para implementar el manejo de error en la aplicación front escrita con Angular.

¿Qué construirá?

Este tutorial lo hacemos como una extensión al presentado en internacionalización en Angular.

Extendemos la aplicación para:

¿Qué necesita?

Antes de realizar este tutorial Ud. ya desarrolló el tutorial de I18N. Es importante tener claro:

Cuando la aplicación front llama a los servicios API REST, estos pueden retornar un código HTTP de error. Al finalizar el tutorial, podrá:

En términos generales, el patrón de diseño Observable/Observer permite que el Observable notifique a quienes lo observan cuando su tarea termina con éxito o en error.

En Angular un objeto Observable puede ser utilizado para distintos tipos de tareas, principalmente para pasar mensajes entre partes de la aplicación, ya sea informando de eventos o cuando tareas asíncronas terminen.

El caso típico es una función que retorna un Observable, la cual encapsula una tarea que se activa cuando un Observer se suscribe al observable.

En la Figura 1 vemos el ejemplo de nuestro código. El servicio BookService declara una función que invoca http.get que retorna un Observable. En el componente, BookListComponent, otra función se suscribe al observable. Cuando se ejecute la suscripción, se activa la función del Observable, el http.get, y cuando termine de traer los valores del backend, se ejecuta la función que está de parámetro en el suscribe; en este caso, los libros que retornó se los asigna a un atributo del componente:

books => this.books = books

Figura 1. Código de ejemplo.

La función subscribe puede recibir los argumentos next, error y complete.

En nuestro ejemplo de la Figura 2, podemos agregar en el componente BookListComponent, en el subscribe de la función getBooks los argumentos next y error.

Esta función imprime en la consola el error.

getBooks(): void {

this.bookService.getBooks().subscribe({next: books =>

this.books = books , error: e => console.error(e)});

}

Figura 2. Ejemplo del argumento error la función Subscribe.

Si en el servicio BookService alteramos la variable apiUrl por un endpoint que no existe (por ejemplo private apiUrl: string = environment.baseUrl + 'books/s';) vamos a obtener en la consola del navegador un error.

Figura 3. Error en la consola del browser.

También podemos identificar el error en el llamado http, es decir en el servicio. La Figura 4 muestra el procedimiento. Note que hay tres llamados: pipe, catchError y throwError. Recuerde importar catchError y throwError.

getBooks(): Observable {

return this.http.get(this.apiUrl).pipe(

catchError(err => throwError(()=> new Error('Error en el servicio')))

);

}

Figura 4. Manejo del error.

Veamos en detalle cada uno:

Veamos el resultado de la ejecución con el nuevo código en el servicio (ver Figura 5).

Figura 5. Error en la consola del navegador.

Angular ofrece una librería, con un conjunto de servicios, para interceptar las peticiones HTTP ya sea, al momento de la salida de la petición hacia el API REST, o al regreso de la petición desde el API REST.

La Figura 7 ilustra el patrón de diseño Interceptor en el contexto de una aplicación Angular. En la figura tenemos dos componentes que utilizan dos servicios. Estos servicios hacen llamados HTTP, por ejemplo http.get(...) o http.post(...). El llamado puede ser interceptado por una interceptor tanto a la salida como al regreso

Figura 7. Patrón de diseño Interceptor.

Si queremos preprocesar o postprocesar la invocación del request en un solo lugar de la aplicación, por ejemplo, agregando información para autenticación, o postprocesar el retorno, podemos "factorizar" estos procesamientos y escribirlos en los interceptores .

Dependiendo de las necesidades de nuestra aplicación, podemos escribir varios interceptores y decidir el orden en el que estos deben ejecutarse.

En este tutorial vamos a interceptar los mensajes de error enviados desde el API REST ya sea porque el back envió un error 412 de una regla de negocio que falló o porque envió un error 500 de un fallo general en la aplicación. Debemos comunicarle al usuario cuál es el problema. El mensaje que vamos a mostrar será el que enviamos en las excepciones BusinessLogicException y WebApplicationException.

Para hacer esto necesitamos dos cosas:

Angular define una interfaz que debe ser implementada por la clase de nuestro interceptor.

interface HttpInterceptor {
  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>>
}

En términos generales, un interceptor debe ser un servicio, es decir decorado con @Injectable(...), y debe implementar el método intercept de la interface HttpInterceptor.

Sin embargo, para facilitar el trabajo, Angular ofrece clases apropiadas para interceptar el error de tipo HttpErrorResponse cuya estructura tiene los atributos desagregados para identificar el tipo de error y el mensaje.

Creamos un folder denominado interceptors en la raíz de nuestra aplicación (app) y allí creamos un servicio denominado HttpErrorInterceptorService que extiende de HttpErrorResponse. Veamos una primera versión del interceptor:

import { HttpErrorResponse, HttpRequest, HttpHandler, HttpEvent } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
@Injectable({
  providedIn: 'root'
})
export class HttpErrorInterceptorService extends HttpErrorResponse {

  constructor(init: any) { super(init); }
  intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    return next.handle(request);
  }
}

El método intercept está definido en la clase HttpErrorResponse. El método recibe un HttpRequest y retorna un Observable. Recibe también el siguiente interceptor suponiendo que hubiera más de uno. El último interceptor será el llamado real al API REST.

En el código anterior, el interceptor no hace nada, solo pasa el request, en este caso al backend.

Ahora veamos el código con el llamado al catchError:

import { HttpErrorResponse, HttpRequest, HttpHandler, HttpEvent } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';

@Injectable({
  providedIn: 'root'
})
export class HttpErrorInterceptorService extends HttpErrorResponse {

  constructor(init: any) { super(init); }

  intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    return next.handle(request ).pipe(catchError((error: HttpErrorResponse) => {
      console.log(error.error.message);
      return throwError(error);
    }));
  }
}

Tenemos el pipe para procesar el resultado, el llamado al catchError para manejar el error y el throwError para disparar el error a los observadores (los suscriptores).

Ahora tenemos que identificar cuál es el tipo de error. ¿Se produjo en el lado del cliente? ¿Se produjo en el backend con algún código HTTP? o ¿está dañada la conexión?

Cuando algo sale mal en el lado del cliente, como un error de red que impide que la solicitud se complete correctamente o una excepción lanzada en un operador RxJS, se producen objetos de tipo ErrorEvent. Si no se trata de un problema en el lado del cliente, entonces la respuesta a la petición viene con un código de error HTTP. Este valor se puede consultar en el atributo status del objeto HttpErrorResponse.

El siguiente código corresponde al interceptor completo. Nos falta ajustar el despliegue del mensaje al usuario. Para esto utilizamos una librería llamada ngx-toastr que explicaremos más adelante.

import {
  HttpEvent,
  HttpHandler,
  HttpRequest,
  HttpErrorResponse
 } from '@angular/common/http';
 import { Observable, throwError } from 'rxjs';
 import { catchError } from 'rxjs/operators';
 import { ToastrService } from 'ngx-toastr';
 import { Injectable } from '@angular/core';
 
 
 @Injectable()
 export class HttpErrorInterceptorService extends HttpErrorResponse {
  constructor(private toastrService: ToastrService) { super(toastrService) }
  intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    return next.handle(request)
      .pipe(
        catchError((httpErrorResponse: HttpErrorResponse) => {
          let errorMesagge = '';
          let errorType = '';
 
 
          if (httpErrorResponse.error instanceof HttpErrorResponse) {
            errorType = "Client side error"
            errorMesagge = httpErrorResponse.statusText;
          } else {
            errorType = "Server side error"
            if (httpErrorResponse.status === 0) {
              errorMesagge = "No hay conexión con el servidor";
            } else {
              errorMesagge = `${httpErrorResponse.status}: ${httpErrorResponse.statusText}`;
            }
           
            if (httpErrorResponse.statusText !== 'OK') {
              this.toastrService.error(errorMesagge, errorType, { closeButton: true })
            }
          }
          return throwError(()=> new Error(errorMesagge));
        })
      )
  }
 }

Una vez creado el interceptor, debemos declararlo en la aplicación, es decir en el módulo principal AppModule. Para esto, además de importar el archivo y las librerías Angular necesarias, debemos indicar que tenemos un interceptor http y que quien lo provee es la clase que definimos.

Veamos esto en el código:

import { NgModule } from '@angular/core';
import { BrowserModule, provideClientHydration } from '@angular/platform-browser';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { BookModule } from './book/book.module';
import { EditorialModule } from './editorial/editorial.module';
import { HTTP_INTERCEPTORS, HttpClientModule } from '@angular/common/http';
import { AuthorModule } from './author/author.module';
import { BookRoutingModule } from './book/book-routing.module';
import { AuthorRoutingModule } from './author/author-routing.module';
import { EditorialRoutingModule } from './editorial/editorial-routing.module';
import { HttpErrorInterceptorService } from './interceptors/HttpErrorInterceptor.service';

@NgModule({
  declarations: [
    AppComponent,
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,
    BookModule,
    EditorialModule,
    HttpClientModule,
    AuthorModule,
    BookRoutingModule,
    AuthorRoutingModule,
    EditorialRoutingModule
  ],
  providers: [
    provideClientHydration(),
    {
      provide: HTTP_INTERCEPTORS,
      useClass: HttpErrorInterceptorService,
      multi: true
    }
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }


Para desplegar el mensaje de error hacemos uso de la librería ngx-toastr.

Figura 8. Errores con toastr

Paso 1: Instalar ngx-toastr

Primero debemos instalar el paquete y salvar la referencia a la dependencia en package.json. Desde la raíz del proyecto ejecutamos:

npm install ngx-toastr --save

Ahora en AppModule debemos importar los archivos y los módulos ToastrModule y BrowserAnimationsModule en el atributo imports del decorador del módulo:

...
import { ToastrModule } from 'ngx-toastr';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
@NgModule({
  ...,
  imports: [
 ...
    ToastrModule.forRoot(),
    BrowserAnimationsModule
  ],
 ...
})
export class AppModule { }

Paso 2: Incluir en la aplicación los estilos del toastr

En el archivo angular.json debemos incluir los estilos del toastr, de otro modo nos saldrán los mensajes vacíos.

 
"styles": [
       "node_modules/ngx-toastr/toastr.css",
       "node_modules/bootstrap/dist/css/bootstrap.min.css",
       "src/styles.css"
            ],

Para que los anteriores cambios surtan efecto debe parar el servidor e iniciarlo nuevamente.

Paso 3: Invocar el toastr

El Módulo ToastrModule ofrece un servicio llamado ToastrService que debe ser inyectado en el componente o servicio donde se quiera utilizar.

Por ejemplo, si volvemos al código del interceptor que definimos antes, tenemos que el HttpErrorInterceptor inyecta el ToastrService declarandolo en su constructor:

...
import { ToastrService } from 'ngx-toastr';

@Injectable()
export class HttpErrorInterceptor extends HttpErrorResponse {
  constructor(private toastrService: ToastrService) { super(toastrService) }
...
}

Para desplegar el mensaje de error, utilizamos la función error del toastrService, que recibe el mensaje, el título del mensaje y un objeto con opciones de configuración que veremos más adelante.

this.toastrService.error(errMsg, errorType, {closeButton: true});

De manera general el servicio tiene las siguientes funciones: success/error/warning/info/show().

Cada una tiene una apariencia por defecto: verde para sucess, amarillo para warning, azul para info y rojo para error (ver Figura 9).

Figura 9. Ejemplos de mensajes.

Paso 4: Configurar el toastr

Los métodos del servicio del toastr pueden recibir de parámetro un objeto de configuración. La documentación completa la pueden encontrar en ngx-toastr. También, existe toastr que es una utilidad que permite ensayar configuraciones no sólo sobre la forma del despliegue sino también sobre la forma como se puede quitar de la pantalla, cuánto tiempo dura, etc.

Veamos un ejemplo de configuración donde decimos que el mensaje se demora 4 segundos, que aparecerá una barra de progreso indicando el paso del tiempo y que se esconderá sin acción del usuario. :

this.toastService.show('Mensaje estándar', {
      delay: 3000,
      progressBar: true,
      autohide: true
    });

Podemos definir configuraciones globales en la aplicación. Esto se debe hacer en el imports del módulo ToastrModule en el AppModule. En este ejemplo estamos indicando que los mensajes se demorarán 10 segundos, que aparecerán en la parte inferior derecha de la aplicación y que no saldrá el mismo toastr más de una vez.

@NgModule({
  ...
  imports: [
   ...
     ToastrModule.forRoot({
      timeOut: 10000,
      positionClass: 'toast-bottom-right',
      preventDuplicates: true,
    }),
    ...
})
export class AppModule { }

Si volvemos a ejecutar nuestro ejemplo en el que hemos cambiado a propósito la url del servicio para producir un error, tenemos el mensaje:

Figura 10. Mensaje de error.

Tenga en cuenta que en el repositorio de este ejemplo cuando se consulta el listado de libros se está lanzando un error a propósito. Si quiere evitar esto recuerde que debe ajustar la variable apiUrl en el servicio BookService así:

private apiUrl: string = environment.baseUrl + 'books';

Al crear el servicio del interceptor se agrega su prueba. Cuando se ejecutan las pruebas se obtendrá un error:

Para solucionarlo, en la prueba del interceptor se debe agregar la referencia al módulo Toastr así:

imports: [ToastrModule.forRoot()],

Ahora las pruebas por defecto del interceptor se ejecutarán correctamente.