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.
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:
true albo false.Trzy pierwsze pracują na drodze do metody kontrolera. Filtr pracuje na drodze powrotnej, i to tylko wtedy, gdy w tej drodze padł wyjątek.
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".Powyższy kod wykonuje zawsze tę samą sekwencję i warto ją znać jako całość:
catch().switchToHttp().exception.getStatus().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.Pole
statusCode niesie najważniejszą informację, więc dobierz je świadomie: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.
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.
@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, @name: nazwy klas wyjątków z Twojego kodu nie powinny nigdy wyjść na zewnątrz.
Urząd kryzysowy działa, meldunki mają jeden formularz:
@Catch(TypWyjatku) zawęża filtr do wskazanego typu; @Catch() bez argumentu łapie wszystko,host.switchToHttp() pobiera kontekst HTTP z ArgumentsHost, dając dostęp do Request i Response - niczego nie przełącza,timestamp i path w odpowiedzi oszczędzą Ci później czasu,@UseFilters na metodzie, @UseFilters na klasie, app.useGlobalFilters() globalnie,@Catch przyjmuje wiele typów; wewnątrz rozróżniasz je przez instanceof i mapujesz na właściwe kody HTTP,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.