En este tutorial podrá seguir los pasos para implementar el manejo de error en la aplicación front escrita con Angular.
Este tutorial lo hacemos como una extensión al presentado en internacionalización en Angular.
Extendemos la aplicación para:
HttpErrorResponse
de Angular que se ocupa de interceptar el error. 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.
next
: indica qué hacer cuando el proceso se termine exitosamente. Es la que se está usando actualmenteerror
: es opcional e indica qué hacer cuando el proceso termine en errorcomplete
: indica qué hacer cuando el proceso terminó bien y se ejecutó la primera función.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.
|
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
.
|
Figura 4. Manejo del error. |
Veamos en detalle cada uno:
pipe
: permite "encauzar" ordenadamente el resultado del Observable a otras funciones. En este caso, a la función que va a atrapar el error catchErrorcatchError
: atrapa el error dele http.get antes de subscribethrowError
: dispara el error y lo puede atrapar subscribeVeamos 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 HttpIntercepto
r
.
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 |
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 { }
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.
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. |
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.