Expecting the Unexpected – Best practices for Error handling in Angular

“Esperar o inesperado mostra um intelecto completamente moderno”. – Oscar Wilde

Este artigo é sobre a centralização da manipulação de erros em Angular. Eu discuto alguns dos tópicos mais comuns como:

  • erros do lado do cliente
  • erros do lado do servidor
  • notificação do usuário
  • erros de rastreamento

Apresento alguns trechos de código durante o caminho e, por último, forneço um link para o exemplo completo.

Versão espanhola:

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

A quem devemos culpar por erros?Link para esta seção

Por que temos erros em nossas aplicações? Por que não podemos escrever código a partir de especificações que sempre funcionam?

Ultimamente, os seres humanos criam software, e nós somos propensos a cometer erros. Algumas razões por trás dos erros podem ser:

  1. Complexidade da aplicação
  2. Comunicação entre partes interessadas
  3. Erros do desenvolvedor
  4. Pressão do tempo
  5. Falta de testes

Esta lista pode continuar e continuar. Com isto em mente, chega a hora em que o inesperado acontece, e um erro é lançado.

Catch them if you canLink to this section

Para pegar exceções síncronas no código, podemos adicionar um bloco try/catch. Se um erro for lançado dentro de try, então nós catch e manipulá-lo. Se não fizermos isto, a execução do script pára.

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

Understandably, isto torna-se insustentável muito rapidamente. Não podemos tentar apanhar erros em qualquer parte do código. Precisamos de manipulação global de erros.

Catch’em allLink para esta seção

Felizmente, Angular fornece um gancho para manipulação centralizada de exceções com ErrorHandler.

A implementação padrão de ErrorHandler imprime mensagens de erro para o console.

Podemos modificar este comportamento criando uma classe que implementa o ErrorHandler:

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

Então, disponibilizamos em nosso módulo raiz para alterar o comportamento padrão em nossa aplicação. Em vez de usar a classe ErrorHandler padrão, estamos usando nossa classe.

<>Copy
@NgModule({ providers: })

Então agora só temos um lugar onde alterar o código para manipulação de erros.

Client-side errorsLink para esta seção

No lado do cliente, quando algo inesperado acontece, um erro de JavaScript é lançado. Ele tem duas propriedades importantes que podemos usar.

  1. message – Descrição legível por humanos do erro.
  2. stack – Traço de pilha de erros com um histórico (call stack) de quais arquivos foram ‘responsáveis’ por causar esse erro.

Tipicamente, a propriedade da mensagem é o que mostramos ao usuário se não escrevermos nossas mensagens de erro.

Server-side errorsLink para esta seção

No lado do servidor, quando algo dá errado, um HttpErrorResponse é retornado. Como com o erro JavaScript, ele tem uma propriedade de mensagem que podemos usar para notificações.

Ele também retorna o código de status do erro. Estes podem ser de diferentes tipos. Se ele começa com um quatro (4xx), então o cliente fez algo inesperado. Por exemplo, se obtivermos o status 400 (Bad Request), então o pedido que o cliente enviou não era o que o servidor estava esperando.

Statuses começando com cinco (5xx) são erros do servidor. O mais típico é o 500 Internal Server Error, um código de status HTTP muito geral que significa que algo deu errado no servidor, mas o servidor não poderia ser mais específico sobre qual é o problema exato.

Com diferentes tipos de erros, é útil com um serviço que analisa as mensagens e pilha traços delas.

Error serviceLink para esta seção

Neste serviço, adicionamos a lógica para analisar as mensagens de erro e pilha traços do servidor e cliente. Este exemplo é muito simplista. Para casos de uso mais avançado podemos usar algo como stacktrace.js.

A lógica neste serviço depende do tipo de erros que recebemos do nosso backend. Depende também do tipo de mensagem que queremos mostrar aos nossos utilizadores.

Usualmente, nós não mostramos o stack trace aos nossos utilizadores. No entanto, se não estivermos em um ambiente de produção, podemos querer mostrar o stack trace para os testadores. Nesse cenário, podemos definir uma bandeira que mostra o stack trace.

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 para esta secção

HttpInterceptor foi introduzido com Angular 4.3.1. Ele fornece uma forma de interceptar solicitações e respostas HTTP para transformá-las ou lidar com elas antes de passá-las.

Existem dois casos de uso que podemos implementar no interceptador.

Primeiro, podemos tentar novamente a chamada HTTP uma ou várias vezes antes de lançarmos o erro. Em alguns casos, por exemplo, se obtivermos um timeout, podemos continuar sem atirar a exceção.

Para isso, usamos o operador de re-tentacao do RxJS para voltar a se inscrever no observavel.

Exemplos mais avançados deste tipo de comportamento:

  • Retriza uma sequência observável sobre o erro baseado em critérios personalizados
  • Potência do RxJS ao usar backoff exponencial

Podemos então verificar o estado da excepção e ver se se trata de um erro 401 não autorizado. Com a segurança baseada no token, podemos tentar actualizar o token. Se isto não funcionar, podemos redireccionar o utilizador para a página de login.

>Cópia
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); } }) ); }}

Aqui podemos voltar a tentar uma vez antes de verificarmos o estado do erro e voltarmos a lançar o erro. Atualizar as fichas de segurança está fora do escopo deste artigo.

>

Tambem precisamos fornecer o interceptador que criamos.

>
>Copiar
providers:

NotificaçõesLink para esta seção

Para notificações, estou usando a Barra de Aperitivos de Material Angular.

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

Com isto, temos notificações simples para o utilizador quando ocorrem erros.

Podemos tratar os erros do lado do servidor e do lado do cliente de forma diferente. Em vez de notificações, podemos mostrar uma página de erro.

Error MessageLink para esta secção

As mensagens de erro são importantes e devem, portanto, ter algum significado para ajudar o utilizador a mover-se. Ao mostrar "Um erro ocorreu" não estamos dizendo ao usuário qual é o problema ou como resolvê-lo.

Em comparação, se em vez disso mostrarmos algo como "Desculpe, você está sem dinheiro", então o usuário sabe qual é o erro. Um pouco melhor mas não os ajuda a resolver o erro.

Uma solução ainda melhor seria dizer-lhes para transferir mais dinheiro e dar um link para uma página de transferência de dinheiro.

Lembrar que a manipulação de erros não substitui o mau UX.

O que eu quero dizer com isto é que você não deve ter nenhum erro esperado. Se um utilizador pode fazer algo que lhe atire um erro, então repare-o!

Não deixe passar um erro só porque criou uma boa mensagem de erro para ele.

LoggingLink para esta secção

Se não registarmos erros, então só o utilizador que os encontrar sabe sobre eles. Salvar a informação é necessário para poder resolver o problema mais tarde.

Quando decidimos armazenar os dados necessários, escolhemos também como salvá-los. Mais sobre isso mais tarde.

Onde devemos guardar os dados?

Com o tratamento centralizado de erros, não temos de ter muita pena de deixar a decisão para mais tarde. Temos apenas um lugar para alterar o nosso código agora. Por enquanto, vamos registrar a mensagem no console.

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

Error TrackingLink para esta seção

Idealmente, você quer identificar bugs em sua aplicação web antes que os usuários os encontrem. O rastreamento de erros é o processo de identificar problemas proativamente e corrigi-los o mais rápido possível.

Então, não podemos simplesmente sentar e esperar que os usuários nos informem os erros. Ao invés disso, devemos ser proativos através do registro e monitoramento de erros.

Deveríamos saber sobre erros quando eles acontecem.

Poderíamos criar nossa solução para este propósito. No entanto, porquê reinventar a roda quando existem tantos serviços excelentes como Bugsnag, Sentry, TrackJs e Rollbar especializados nesta área.

Utilizar uma destas soluções de acompanhamento de erros front-end pode permitir-lhe gravar e reproduzir sessões de utilizadores para que possa ver por si próprio exactamente o que o utilizador experimentou.

Se você não consegue reproduzir um erro, então você não pode corrigi-lo.

Em outras palavras, uma solução adequada de rastreamento de erros poderia alertá-lo quando um erro ocorre e fornecer informações sobre como replicar/resolver o problema.

Em um artigo anterior, Como enviar Erros para Slack in Angular Eu falei sobre o uso de Slack para rastrear erros. Como exemplo, poderíamos usá-lo aqui:

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 uma solução mais robusta está fora do escopo deste artigo.

Todos juntos agoraLigue a esta seção

Porque o manuseio de erros é essencial, ele é carregado primeiro. Por causa disso, não podemos usar a injeção de dependência no construtor para os serviços. Em vez disso, temos que injetá-los manualmente com Injector.

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

ConclusionLink para esta seção

O manuseio de erros é uma pedra angular para uma aplicação empresarial. Neste artigo, nós o centralizamos sobrepondo o comportamento padrão do ErrorHandler. Nós então adicionamos vários serviços:

  1. Serviço de erro para analisar mensagens e traços de pilha.
  2. Serviço de notificação para notificar usuários sobre erros.
  3. Serviço de registro para registrar erros.

Aplicamos também uma classe interceptor para:

  1. Retria antes de lançar o erro.
  2. Checar erros específicos e responder de acordo.

Com esta solução, você pode começar a rastrear seus erros e esperançosamente dar aos usuários uma melhor experiência.

Examplo código no GitHub.
Executar o código no StackBlitz.

ResourcesLink para esta seção

  • Tratamento de Erros & Angular por Aleix Suau
  • Tratamento de Erros em Javascript: The Definitive Guide by Lukas Gisder-Dubé
  • Manuseio Global de Erros com Angular 2+ por Austin
  • Aplicações angulares - manipulação de erros e recuperação elegante por Brachi Packter
  • A Arte da Mensagem de Erro por Marina Posniak
  • JavaScript Weekly: Graceful Error Handling by Severin Perez
Discuta com a comunidade

Leave a Reply