¿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 Desplegar el detalle de un libro.

Extendemos la aplicación para:

¿Qué necesita?

Antes de realizar este tutorial Ud. ya desarrolló el tutorial de Desplegar el detalle de un libro. Es importante tener claro:

Para ejecutar el resultado final de este taller Ud. debe tener en ejecución sobre payara, el proyecto backstepbystep. No olvide inicializar la base de datos y ejecutar el sql que inserta los datos.

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 termine, 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, 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 recibe tres funciones: subscribe (f1, f2, f3)

En nuestro ejemplo de la Figura 2, podemos agregar, f2 y f3. f2 imprime en la consola el error (en el rectángulo rojo).

Figura 2. Ejemplo de la función Subscribe.

Si por ejemplo, alteramos la URL por un valor que no existe vamos a obtener en la consola el error de la Figura 3.

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.

Figura 4. Manejo del error.

Veamos en detalle cada uno:

La Figura 5 muestra el código del servicio y el código del componente con el manejo del error en ambos lados:

Figura 5. Código del servicio

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

Figura 6. 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:

Crear un interceptor que atrape el regreso de un llamado HTTP e identifique si viene con error.

Agregar en el interceptor una instrucción para que muestre el error al usuario.

Angular define una interface 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 HttpErrorResponse cuya estructura la trae los atributos desagregados para identificar el tipo de error, el mensaje.

Creamos un folder en la raíz de nuestra aplicación (app) que se llame interceptors y allí creamos un servicio 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 del 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 explicar el despliegue del mensaje al usuario. Para esto utilizamos una librería llamada el toastr y 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 HttpErrorInterceptor extends HttpErrorResponse {
  constructor(private toastrService: ToastrService) { super(toastrService) }
  intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    return next.handle(request)
      .pipe(
        catchError((error: HttpErrorResponse) => {
          let errMsg = '';
          let errorType = 'Error';
          // Client Side Error
          if (error.error instanceof ErrorEvent) {
            errMsg = `Error: ${error.error.message}`;
          } else {  // Server Side Error
            if (error.status === 0) {
              errMsg = `${error.status}, "No hay conexión con el servidor"`;
              errorType = 'Major Error';
            } else {
              errMsg = `${error.status}: ${error.error}`;
            }
            this.toastrService.error(errMsg, errorType, { closeButton: true });
          }
          console.log(errMsg);
          return throwError(errMsg);
        })
      )
  }
}

Una vez creado el interceptor, debemos declararlo en la aplicación, es decir en el módulo principal AppModule. Para esto, aparte de importar el archivo y las librería 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 { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { HttpErrorInterceptor } from './interceptors/http-error-interceptor.service';


import { AppComponent } from './app.component';
import { BookModule } from './book/book.module';
import { EditorialModule } from './editorial/editorial.module';
import { AuthorModule } from './author/author.module';

@NgModule({
  declarations: [
    AppComponent,

  ],
  imports: [
    BrowserModule,
    HttpClientModule,
    BookModule,
    EditorialModule,
    AuthorModule
  ],
  bootstrap: [AppComponent],
  providers: [
    {
      provide: HTTP_INTERCEPTORS,
      useClass: HttpErrorInterceptor,
      multi: true
    }
  ]
})
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

También es necesario instalar animations de Angular:

npm install @angular/animations --save

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"
            ],

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 toster 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 bar 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. Estamos indicando que los mensajes se demorarán 10 segundos,m 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, recuerden que a propósito cambiamos la url del servicio para producir un error, tenemos el mensaje:

Figura 10. Mensaje de error.