Kurs NestJS · Moduł 6: Obsługa błędów i monitoring
Exception Filters - zarządzanie kryzysami
W tej lekcji7
Legionista pyta o legion, którego nie ma. Zamiast czytelnej odmowy dostaje ścianę tekstu: nazwę klasy błędu, ślad stosu, ścieżki plików z Twojego serwera. Kolejny endpoint zwraca w podobnej sytuacji zupełnie inny kształt odpowiedzi, bo ktoś obsłużył błąd ręcznie. Klient nie wie, czego się spodziewać, a Ty właśnie pokazałeś światu budowę swojego obozu.
Rzym miał na kryzysy osobny urząd. Gdy w prowincji coś zawiodło, meldunek nie szedł do Senatu w postaci, w jakiej przyszedł z pola - urzędnik przepisywał go na jednolity formularz. W NestJS tym urzędnikiem jest Exception Filter.
Czym jest, a czym nie jest
Exception Filter to klasa przechwytująca wyjątki i formatująca odpowiedź błędu. To jedyne jego zadanie i warto je odgraniczyć od sąsiadów, bo wszystkie cztery elementy stoją na drodze żądania:
- Middleware działa najwcześniej, na surowym żądaniu, zanim NestJS wie, dokąd ono trafi.
- Guard decyduje, czy wpuścić - zwraca
truealbofalse. - Pipe waliduje i przekształca dane wejściowe.
- Exception Filter wkracza jako ostatni, dopiero gdy coś poszło nie tak, i decyduje, co zobaczy klient.
Trzy pierwsze pracują na drodze do metody kontrolera. Filtr pracuje na drodze powrotnej, i to tylko wtedy, gdy w tej drodze padł wyjątek.
@Catch - wybór, czym się zajmujemy
Filtr zaczyna się od dekoratora mówiącego, jakie wyjątki obsługuje:
1@Catch(HttpException)
2export class LegionHttpExceptionFilter implements ExceptionFilter {
3 catch(exception: HttpException, host: ArgumentsHost) {
4 const ctx = host.switchToHttp();
5 const response = ctx.getResponse<Response>();
6 const request = ctx.getRequest<Request>();
7
8 const status = exception.getStatus();
9
10 response.status(status).json({
11 statusCode: status,
12 timestamp: new Date().toISOString(),
13 path: request.url,
14 message: exception.message,
15 });
16 }
17}@Catch(HttpException) zawęża filtr do jednego typu wyjątku - ten obsłuży tylko wyjątki HTTP, a wszystko inne przepuści dalej. Sam dekorator bez argumentu, @Catch(), złapałby absolutnie wszystko; to przydaje się dla filtra ostatniej szansy, ale wtedy tracisz informację o typie.
Metoda catch przyjmuje dwa argumenty. Pierwszy to sam wyjątek. Drugi, host typu ArgumentsHost, jest ciekawszy: NestJS obsługuje nie tylko HTTP, ale też WebSockety i mikroserwisy, więc host trzyma kontekst w postaci niezależnej od protokołu. host.switchToHttp() pobiera z niego kontekst HTTP, dając dostęp do obiektów Request i Response. Nie przełącza żadnego protokołu - jedynie mówi "potraktuj ten kontekst jako HTTP".
Pięć kroków obsługi
Powyższy kod wykonuje zawsze tę samą sekwencję i warto ją znać jako całość:
- Przechwycenie wyjątku w metodzie
catch(). - Pobranie kontekstu HTTP przez
switchToHttp(). - Odczytanie statusu i danych wyjątku -
exception.getStatus(). - Sformatowanie odpowiedzi JSON - jednolity kształt dla całej aplikacji.
- Wysłanie odpowiedzi do klienta przez
response.status(...).json(...).
Zwróć uwagę, czego w tej odpowiedzi nie ma: śladu stosu ani nazw plików. To celowe - stos loguj po stronie serwera, klientowi wysyłaj tylko to, co pomoże mu poprawić żądanie. Dodane timestamp i path kosztują nic, a przy zgłoszeniu błędu od użytkownika oszczędzają godzinę szukania.
Kody HTTP - język odmowy
Pole statusCode niesie najważniejszą informację, więc dobierz je świadomie:
- 400 Bad Request - żądanie jest źle zbudowane, klient musi je poprawić.
- 401 Unauthorized - nie wiadomo, kim jesteś; zaloguj się.
- 403 Forbidden - wiadomo, kim jesteś, i nie wolno Ci.
- 404 Not Found - zasobu nie znaleziono.
- 500 Internal Server Error - zawiódł serwer, nie klient.
Granica między 4xx a 5xx jest tu istotna: czwórki mówią „popraw żądanie", piątki mówią „to nasza wina". Zwracanie 500 przy błędnych danych wejściowych wysyła klienta na poszukiwanie usterki, której u niego nie ma.
Zasięg - gdzie postawić urzędnika
Ten sam filtr można umieścić na trzech poziomach, od najwęższego do najszerszego:
1// 1. Na metodzie kontrolera - najwęziej
2@Get(':id')
3@UseFilters(LegionHttpExceptionFilter)
4findOne(@Param('id') id: string) { }
5
6// 2. Na klasie kontrolera - wszystkie jego metody
7@Controller('legions')
8@UseFilters(LegionHttpExceptionFilter)
9export class LegionsController { }
10
11// 3. Globalnie - cała aplikacja
12app.useGlobalFilters(new LegionHttpExceptionFilter());Reguła jest prosta: im niżej postawisz filtr, tym węższy jego zasięg. Filtr na metodzie obsłuży tylko ją, na klasie - wszystkie metody kontrolera, globalny - całą aplikację.
Zapamiętaj tę kolejność, bo wraca przy guardach, pipe'ach i interceptorach - w NestJS wszystkie cztery stosuje się tak samo. W praktyce jeden filtr globalny nadaje wspólny kształt odpowiedzi, a filtry węższe dokładają obsługę specyficzną dla wycinka aplikacji.
Jeden filtr, kilka typów wyjątków
@Catch przyjmuje wiele typów naraz - przydaje się, gdy pochodzą z jednego źródła i wymagają podobnej obsługi:
1@Catch(QueryFailedError, EntityNotFoundError)
2export class DatabaseExceptionFilter implements ExceptionFilter {
3 catch(exception: Error, host: ArgumentsHost) {
4 const response = host.switchToHttp().getResponse<Response>();
5
6 const status =
7 exception instanceof EntityNotFoundError
8 ? HttpStatus.NOT_FOUND
9 : HttpStatus.BAD_REQUEST;
10
11 response.status(status).json({
12 statusCode: status,
13 message:
14 status === HttpStatus.NOT_FOUND
15 ? 'Nie znaleziono zasobu'
16 : 'Nieprawidłowe zapytanie do bazy danych',
17 });
18 }
19}Oba wyjątki pochodzą z TypeORM, ale znaczą co innego, więc wewnątrz catch rozróżniamy je przez instanceof. EntityNotFoundError to 404 - klient poprosił o coś, czego nie ma. QueryFailedError to 400, bo zwykle wynika z danych, które klient przysłał.
I tu widać drugą wartość filtra, poza jednolitym kształtem: tłumaczy wyjątki wewnętrzne na język HTTP. Klient nie musi wiedzieć, że używasz TypeORM - dostaje kod i komunikat, które coś dla niego znaczą. Trzymaj się tej granicy: nazwy klas wyjątków z Twojego kodu nie powinny nigdy wyjść na zewnątrz.
Podsumowanie
Urząd kryzysowy działa, meldunki mają jeden formularz:
- Exception Filter to klasa przechwytująca wyjątki i formatująca odpowiedź błędu - nie Middleware, nie Guard, nie Pipe,
- trzy tamte pracują na drodze do kontrolera, filtr - na powrotnej i tylko po wyjątku,
@Catch(TypWyjatku)zawęża filtr do wskazanego typu;@Catch()bez argumentu łapie wszystko,host.switchToHttp()pobiera kontekst HTTP zArgumentsHost, dając dostęp doRequestiResponse- niczego nie przełącza,- pięć kroków: przechwyć, pobierz kontekst, odczytaj status, sformatuj JSON, wyślij,
- do klienta nie wysyłaj śladu stosu;
timestampipathw odpowiedzi oszczędzą Ci później czasu, - 404 to „nie znaleziono zasobu"; 4xx znaczy „popraw żądanie", 5xx - „to nasza wina",
- zasięg od najwęższego:
@UseFiltersna metodzie,@UseFiltersna klasie,app.useGlobalFilters()globalnie, @Catchprzyjmuje wiele typów; wewnątrz rozróżniasz je przezinstanceofi mapujesz na właściwe kody HTTP,- filtr tłumaczy wyjątki wewnętrzne na język HTTP - nazwy Twoich klas nie powinny wychodzić na zewnątrz.
W następnej lekcji nauczysz się tworzyć własne wyjątki, żeby mieć co przechwytywać - z czytelną hierarchią zamiast garści throw new Error. A na razie zapamiętaj: filtr to urzędnik przepisujący meldunek na jednolity formularz - klient dostaje kod i komunikat, a budowa obozu zostaje w obozie.
Kod do tej lekcji: src/filters/exception-filters.ts
1// Exception Filters - Zarzadzanie Kryzysami w Imperium
2// Przechwytywanie i formatowanie bledow
3import {
4 ExceptionFilter,
5 Catch,
6 ArgumentsHost,
7 HttpException,
8 HttpStatus,
9 Logger,
10} from '@nestjs/common';
11import { Request, Response } from 'express';
12
13// ===========================================
14// 1. Podstawowy HttpException Filter
15// ===========================================
16
17@Catch(HttpException)
18export class LegionHttpExceptionFilter implements ExceptionFilter {
19 private logger = new Logger('LegionExceptionFilter');
20
21 catch(exception: HttpException, host: ArgumentsHost) {
22 const ctx = host.switchToHttp();
23 const response = ctx.getResponse<Response>();
24 const request = ctx.getRequest<Request>();
25 const status = exception.getStatus();
26 const exceptionResponse = exception.getResponse();
27
28 // Formatowanie odpowiedzi bledow - styl Imperium
29 const errorResponse = {
30 statusCode: status,
31 timestamp: new Date().toISOString(),
32 path: request.url,
33 method: request.method,
34 message: typeof exceptionResponse === 'string'
35 ? exceptionResponse
36 : (exceptionResponse as any).message,
37 error: HttpStatus[status] || 'Unknown Error',
38 };
39
40 this.logger.error(
41 'Kryzys [' + status + '] ' + request.method + ' ' + request.url
42 + ': ' + errorResponse.message
43 );
44
45 response.status(status).json(errorResponse);
46 }
47}
48
49// ===========================================
50// 2. Globalny filter - lapie WSZYSTKIE wyjatki
51// ===========================================
52
53@Catch()
54export class AllExceptionsFilter implements ExceptionFilter {
55 private logger = new Logger('AllExceptions');
56
57 catch(exception: unknown, host: ArgumentsHost) {
58 const ctx = host.switchToHttp();
59 const response = ctx.getResponse<Response>();
60 const request = ctx.getRequest<Request>();
61
62 const status = exception instanceof HttpException
63 ? exception.getStatus()
64 : HttpStatus.INTERNAL_SERVER_ERROR;
65
66 const message = exception instanceof Error
67 ? exception.message
68 : 'Nieznany blad w systemie Imperium';
69
70 this.logger.error('KRYTYCZNY BLAD: ' + message);
71
72 response.status(status).json({
73 statusCode: status,
74 timestamp: new Date().toISOString(),
75 path: request.url,
76 message: message,
77 });
78 }
79}
80
81// ===========================================
82// 3. Rejestracja filtrow
83// ===========================================
84
85// Globalnie w main.ts:
86// app.useGlobalFilters(new AllExceptionsFilter());
87//
88// Na kontrolerze:
89// @UseFilters(new LegionHttpExceptionFilter())
90//
91// Na metodzie:
92// @UseFilters(LegionHttpExceptionFilter)
93
94console.log('=== Exception Filters ===');
95console.log('@Catch(HttpException) - lapie konkretny typ');
96console.log('@Catch() - lapie WSZYSTKIE wyjatki');
97console.log('host.switchToHttp() - kontekst HTTP');
98console.log('Rejestracja: global, controller, method');
99Widzisz błąd w tej lekcji?
Sprawdź się
Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.
1. Czym jest Exception Filter w NestJS?
2. Co robi dekorator @Catch(HttpException) na klasie filtra?
To 2 z 4 pytań do tej lekcji. Pozostałe rozwiążesz w grze.
Zadania praktyczne w grze
- Edytor kodu
Uzupełnij klasę LegionHttpExceptionFilter z @Catch(HttpException), która implementuje ExceptionFilter z metodą catch(exception, host), pobiera response i request z ArgumentsHost i zwraca JSON z statusCode, timestamp, path i message
- Klikanie w kolejności
Ułóż elementy obsługi wyjątku w Exception Filter w poprawnej kolejności
- Układanie w pionie
Uporządkuj poziomy aplikowania Exception Filter od najwęższego do najszerszego zasięgu
- Edytor kodu
Uzupełnij DatabaseExceptionFilter z @Catch(QueryFailedError, EntityNotFoundError), który w catch() rozróżnia typ wyjątku i zwraca odpowiedni kod HTTP (400 dla QueryFailed, 404 dla EntityNotFound)