Expecting the Unexpected – Best practices for Error handling in Angular

„Oczekiwanie nieoczekiwanego świadczy o na wskroś nowoczesnym intelekcie.” – Oscar Wilde

Ten artykuł jest o centralizacji obsługi błędów w Angularze. Omawiam kilka bardziej popularnych tematów, takich jak:

  • błędy po stronie klienta
  • błędy po stronie serwera
  • powiadomienia użytkownika
  • błędy śledzenia

Prezentuję kilka snippetów kodu w trakcie drogi, a na koniec podaję link do pełnego przykładu.

Wersja hiszpańska:

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

Kogo powinniśmy winić za błędy?Link do tej sekcji

Dlaczego mamy błędy w naszych aplikacjach? Dlaczego nie możemy napisać kodu ze specyfikacji, który zawsze działa?

W końcu to ludzie tworzą oprogramowanie, a my jesteśmy podatni na popełnianie błędów. Niektóre powody błędów mogą być następujące:

  1. Złożoność aplikacji
  2. Komunikacja między zainteresowanymi stronami
  3. Błędy dewelopera
  4. Presja czasu
  5. Brak testów

Ta lista mogłaby się ciągnąć i ciągnąć. Mając to na uwadze, przychodzi czas, kiedy zdarza się coś nieoczekiwanego i rzucany jest błąd.

Catch them if you canLink do tej sekcji

Aby wyłapać w kodzie wyjątki synchroniczne, możemy dodać blok try/catch. Jeśli błąd zostanie rzucony wewnątrz try to my catch go i obsłużymy. Jeśli tego nie zrobimy, wykonanie skryptu zatrzyma się.

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

Zrozumiałe, że bardzo szybko staje się to niezrównoważone. Nie możemy próbować wyłapywać błędów wszędzie w kodzie. Potrzebujemy globalnej obsługi błędów.

Catch’em allLink do tej sekcji

Na szczęście Angular zapewnia hak do scentralizowanej obsługi wyjątków z ErrorHandler.

Domyślna implementacja ErrorHandler drukuje komunikaty o błędach do console.

Możemy zmodyfikować to zachowanie, tworząc klasę, która implementuje ErrorHandler:

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

Potem udostępniamy ją w naszym module głównym, aby zmienić domyślne zachowanie w naszej aplikacji. Zamiast używać domyślnej klasy ErrorHandler używamy naszej klasy.

<>Copy
@NgModule({ providers: })

Teraz mamy więc tylko jedno miejsce, w którym musimy zmienić kod obsługi błędów.

Błędy po stronie klientaLink do tej sekcji

Po stronie klienta, gdy dzieje się coś nieoczekiwanego, rzucany jest błąd JavaScript. Ma on dwie ważne właściwości, które możemy wykorzystać.

  1. message – Czytelny dla człowieka opis błędu.
  2. stack – Ślad stosu błędu z historią (stos wywołań) plików, które były „odpowiedzialne” za spowodowanie tego błędu.

Typowo, właściwość message jest tym, co pokazujemy użytkownikowi, jeśli nie piszemy naszych komunikatów o błędach.

Błędy po stronie serweraLink do tej sekcji

Po stronie serwera, kiedy coś pójdzie nie tak, zwracana jest odpowiedź HttpErrorResponse. Podobnie jak w przypadku błędu JavaScript, posiada on właściwość message, którą możemy wykorzystać do powiadomień.

Zwraca on również kod statusu błędu. Mogą to być różne typy. Jeśli zaczyna się od czwórki (4xx), to znaczy, że klient zrobił coś nieoczekiwanego. Na przykład, jeśli otrzymamy status 400 (Bad Request), to żądanie, które klient wysłał nie było tym, czego serwer oczekiwał.

Statusy zaczynające się od piątki (5xx) są błędami serwera. Najbardziej typowy jest 500 Internal Server Error, bardzo ogólny kod statusu HTTP, który oznacza, że coś poszło nie tak na serwerze, ale serwer nie może być bardziej szczegółowy, jaki jest dokładny problem.

W przypadku różnych rodzajów błędów pomocna jest usługa, która parsuje komunikaty i ślady stosu z nich.

Usługa błędówLink do tej sekcji

W tej usłudze dodajemy logikę parsowania komunikatów o błędach i śladów stosu z serwera i klienta. Ten przykład jest bardzo uproszczony. Dla bardziej zaawansowanych przypadków użycia moglibyśmy użyć czegoś takiego jak stacktrace.js.

Logika w tej usłudze zależy od tego, jakiego rodzaju błędy otrzymujemy z naszego backendu. Zależy to również od tego, jaki rodzaj wiadomości chcemy pokazać naszym użytkownikom.

Zwykle nie pokazujemy śladu stosu naszym użytkownikom. Jednakże, jeśli nie jesteśmy w środowisku produkcyjnym, możemy chcieć pokazać ślad stosu testerom. W takim scenariuszu możemy ustawić flagę, która pokaże ślad stosu.

<>Kopiuj
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 do tej sekcji

HttpInterceptor został wprowadzony wraz z Angular 4.3.1. Zapewnia sposób na przechwytywanie żądań i odpowiedzi HTTP, aby przekształcić lub obsłużyć je przed przekazaniem ich dalej.

Istnieją dwa przypadki użycia, które możemy zaimplementować w interceptorze.

Po pierwsze, możemy ponowić próbę wywołania HTTP raz lub wiele razy, zanim rzucimy błąd. W niektórych przypadkach, na przykład, jeśli otrzymamy timeout, możemy kontynuować bez rzucania wyjątku.

Do tego używamy operatora retry z RxJS, aby ponownie zapisać się do obserwowalnego.

Więcej zaawansowanych przykładów tego typu zachowań:

  • Retry sekwencji obserwowalnej na błąd na podstawie niestandardowych kryteriów
  • Moc RxJS podczas używania exponential backoff

Możemy wtedy sprawdzić status wyjątku i zobaczyć, czy jest to błąd 401 unauthorized. W przypadku zabezpieczeń opartych na tokenach możemy spróbować odświeżyć token. Jeśli to nie zadziała, możemy przekierować użytkownika na stronę logowania.

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

Tutaj ponawiamy próbę raz, zanim sprawdzimy status błędu i ponownie wyrzucimy błąd. Odświeżanie tokenów bezpieczeństwa jest poza zakresem tego artykułu.

Musimy również dostarczyć interceptor, który stworzyliśmy.

<>Copy
providers:

NotyfikacjeLink do tej sekcji

Do powiadomień używam Angular Material Snackbar.

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

Dzięki temu mamy proste powiadomienia dla użytkownika, gdy wystąpią błędy.

Błędy po stronie serwera i klienta możemy obsługiwać inaczej. Zamiast powiadomień, możemy pokazać stronę z błędem.

Wiadomość o błędzieLink do tej sekcji

Wiadomości o błędach mają znaczenie i dlatego powinny mieć jakieś znaczenie, aby pomóc użytkownikowi iść dalej. Pokazując „Wystąpił błąd” nie mówimy użytkownikowi, co jest problemem lub jak go rozwiązać.

W porównaniu, jeśli zamiast tego pokażemy coś takiego jak „Przepraszamy, nie masz pieniędzy”, wtedy użytkownik wie, jaki jest błąd. Trochę lepiej, ale nie pomaga im to w rozwiązaniu błędu.

Jeszcze lepszym rozwiązaniem byłoby powiedzenie im, żeby przelali więcej pieniędzy i podanie linku do strony z przelewem pieniędzy.

Pamiętaj, że obsługa błędów nie jest substytutem złego UX.

To, co mam przez to na myśli, to fakt, że nie powinieneś mieć żadnych oczekiwanych błędów. Jeśli użytkownik może zrobić coś, co rzuca błąd, to napraw to!

Nie przepuszczaj błędu tylko dlatego, że stworzyłeś dla niego ładny komunikat o błędzie.

LogowanieLink do tej sekcji

Jeśli nie logujemy błędów, to wie o nich tylko użytkownik, który na nie wpadł. Zapisywanie informacji jest konieczne, aby móc później rozwiązać problem.

Kiedy zdecydowaliśmy się na zapisywanie danych, musimy również wybrać sposób ich zapisywania. Więcej na ten temat później.

Gdzie powinniśmy zapisać dane?

Dzięki scentralizowanej obsłudze błędów nie musimy mieć wyrzutów sumienia, że zostawiliśmy decyzję na później. Mamy teraz tylko jedno miejsce, w którym możemy zmienić nasz kod. Na razie wylogujmy komunikat do konsoli.

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

Śledzenie błędówLink do tej sekcji

Chcesz identyfikować błędy w swojej aplikacji internetowej, zanim napotkają je użytkownicy. Śledzenie błędów jest procesem proaktywnego identyfikowania problemów i naprawiania ich tak szybko, jak to możliwe.

Nie możemy więc po prostu usiąść i oczekiwać, że użytkownicy zgłoszą nam błędy. Zamiast tego, powinniśmy być proaktywni poprzez rejestrowanie i monitorowanie błędów.

Powinniśmy wiedzieć o błędach, kiedy one się zdarzają.

Moglibyśmy stworzyć nasze rozwiązanie do tego celu. Jednak po co wymyślać koło na nowo, skoro istnieje tak wiele doskonałych usług, takich jak Bugsnag, Sentry, TrackJs i Rollbar specjalizujących się w tej dziedzinie.

Użycie jednego z tych rozwiązań śledzenia błędów front-end może pozwolić na nagrywanie i odtwarzanie sesji użytkownika, dzięki czemu można zobaczyć dokładnie to, czego doświadczył użytkownik.

Jeśli nie możesz odtworzyć błędu, to nie możesz go naprawić.

Innymi słowy, odpowiednie rozwiązanie do śledzenia błędów może powiadomić Cię, gdy wystąpi błąd i zapewnić wgląd w to, jak replikować/rozwiązać problem.

W jednym z wcześniejszych artykułów, Jak wysyłać błędy do Slacka w Angular, rozmawiałem o używaniu Slacka do śledzenia błędów. Jako przykład, moglibyśmy użyć go tutaj:

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

Wdrożenie bardziej solidnego rozwiązania jest poza zakresem tego artykułu.

Wszystko razem terazLink do tej sekcji

Ponieważ obsługa błędów jest niezbędna, zostaje ona załadowana jako pierwsza. Z tego powodu, nie możemy używać wstrzykiwania zależności w konstruktorze dla usług. Zamiast tego musimy wstrzyknąć je ręcznie za pomocą Injectora.

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

ZakończenieLink do tej sekcji

Obsługa błędów jest podstawą aplikacji dla przedsiębiorstw. W tym artykule, scentralizowaliśmy ją poprzez nadpisanie domyślnego zachowania ErrorHandler. Następnie dodaliśmy kilka usług:

  1. Usługa obsługi błędów do parsowania komunikatów i śladów stosu.
  2. Usługa powiadamiania użytkowników o błędach.
  3. Usługa logowania do rejestrowania błędów.

Zaimplementowaliśmy również klasę przechwytującą w celu:

  1. Powtarzania przed wyrzuceniem błędu.
  2. Sprawdzania określonych błędów i odpowiedniego reagowania.

Dzięki temu rozwiązaniu możesz zacząć śledzić swoje błędy i miejmy nadzieję, że dasz użytkownikom lepsze doświadczenie.

Przykładowy kod na GitHub.
Uruchom kod na StackBlitz.

Źródła Link do tej sekcji

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

Dyskutuj z społecznością

Leave a Reply