
Czy architektura throw
+ może catch
to dobry pomysł?
Zwolenników i przeciwników rzucania wyjątkami w aplikacji jest wielu. Jedni uważają to za zły pomysł, inni za dobre rozwiązanie. Warto znać obie strony medalu, aby podjąć świadomą decyzję.
Ten artykuł opiera się o TypeScript/JavaScript, jednak zasady są uniwersalne i można je zastosować w innych językach programowania.
Try/catch w praktyce
Zacznijmy od podstaw - czym jest try/catch
i jak działa w praktyce?
try {
// kod, który może wygenerować błąd
} catch (error) {
// kod obsługujący błąd
}
W bloku try
umieszczamy kod, który może wygenerować błąd. Jeśli błąd wystąpi, zostanie on przechwycony przez blok catch
, gdzie możemy go obsłużyć wykonując alternatywną akcję.
Po co nam obsługa błędów?
Co by się stało, gdybyśmy pominęli obsługę potencjalnych błędów? Odpowiedź jest prosta: program by się zawiesił. W przypadku błędu aplikacja zostałaby zatrzymana, a oczekiwane działanie nie zostałoby zrealizowane.
Kiedy warto używać try/catch?
Warto używać try/catch
w sytuacjach, gdy:
- Wykonujemy operacje zewnętrzne (np. pobieranie danych z API)
- Parsujemy dane (np. JSON/XML)
- Operujemy na plikach
- Komunikujemy się z bazą danych
- Istnieje ryzyko wystąpienia błędu poza naszą kontrolą
W skrócie - try/catch
stosujemy gdy operacja jest poza naszą bezpośrednią kontrolą.
Dlaczego nie warto rzucać wyjątków?
Tworząc funkcję, która rzuca wyjątkiem, konstruujemy potencjalną "bombę zegarową". Nie mamy pewności, czy ktoś prawidłowo obsłuży ten wyjątek w przyszłości.
Spójrzmy na przykład:
// Źle - nie obsługujemy potencjalnych błędów
export const getUserNameFromLocalStorage = (): string => {
const userFromLs = localStorage.getItem('user')!;
return JSON.parse(userFromLs).name;
};
// Lepiej - obsługujemy null i informujemy o wyjatkach
export const getUserNameFromLocalStorage = (): string | never => {
const userFromLs = localStorage.getItem('user');
if (!userFromLs) return '';
return JSON.parse(userFromLs).name;
};
// Najlepiej - pełna obsługa błędów
export const getUserNameFromLocalStorage = (): string => {
const userFromLs = localStorage.getItem('user');
if (!userFromLs) return '';
try {
return JSON.parse(userFromLs).name;
} catch (error) {
console.error('Error while parsing user from local storage', error);
return '';
}
};
W ostatniej wersji:
- Obsługujemy wszystkie potencjalne błędy
- Logujemy problemy
- Zwracamy bezpieczną wartość domyślną
- Kod jest przewidywalny i stabilny
Czy musimy w takiej sytuacji rzucać wyjątkiem? Zdecydowanie nie - mamy przecież pełną kontrolę nad tym, co się dzieje w kodzie.
Ten przykład może wydawać się dość prosty, ale pokazuje istotę problemu. Moglibyśmy go rozbudować wykorzystując mapper assureUser()
(więcej o mapperach znajdziesz w moim artykule Dlaczego nie warto ufać API), ale spójrzmy na to z innej perspektywy.
W aplikacjach backendowych często spotykam się z architekturą throw
+ catch
. Jest to powszechne podejście, ponieważ zazwyczaj na najwyższym poziomie aplikacji mamy error handler, który przechwyci wyjątek i zwróci odpowiedni kod błędu (np. 500 - Internal Server Error). Ale czy na pewno jest to najlepsze rozwiązanie?
Według mnie istnieją lepsze sposoby! Weźmy przykład: mamy skrypt w Node.js, który zapisuje tekst do pliku. Co powinniśmy zrobić, gdy plik nie istnieje? Rzucić wyjątkiem, czy może po prostu zwrócić false
?
A może lepszym rozwiązaniem byłoby obsłużenie tego błędu bezpośrednio w serwisie? Możemy złapać wyjątek i zwrócić odpowiedni enum do controllera, który następnie zwróci właściwy kod błędu w odpowiedzi HTTP. Co więcej, jeśli zapis do pliku jest tylko opcjonalnym efektem ubocznym (side-effect) głównej operacji, możemy zalogować błąd na standardowe wyjście stderr i pozwolić aplikacji kontynuować działanie.
Z tymi przemyśleniami zostawiam Was, drodzy Czytelnicy - choć pewnie już wiecie, jakie jest moje stanowisko w tej kwestii 😉
Podsumowanie
Zamiast architektury throw
+ catch
, lepiej skupić się na przewidywaniu i obsłudze błędów tam, gdzie one występują. Taki kod jest:
- Bardziej przewidywalny
- Łatwiejszy w utrzymaniu
- Bezpieczniejszy w działaniu
- Prostszy w testowaniu
Wyjątki łapmy, ale nie rzucajmy ich sami - nasza aplikacja będzie dzięki temu stabilniejsza i łatwiejsza w rozwoju.
Objaśnienie skrótów
- try/catch - Mechanizm obsługi wyjątków w wielu językach programowania, pozwalający na przechwytywanie i obsługę błędów
- throw - Instrukcja rzucająca wyjątek w kodzie, przerywa normalne wykonanie programu
- API - Application Programming Interface - interfejs programistyczny aplikacji, zestaw reguł i protokołów umożliwiających komunikację między aplikacjami
- JSON - JavaScript Object Notation - format wymiany danych bazujący na języku JavaScript
- XML - eXtensible Markup Language - uniwersalny język znaczników służący do reprezentacji różnych danych w strukturalizowany sposób
- Node.js - Środowisko uruchomieniowe JavaScript działające poza przeglądarką, używane głównie do tworzenia aplikacji serwerowych
- HTTP - Hypertext Transfer Protocol - protokół przesyłania dokumentów hipertekstowych używany w sieci WWW
- never - Typ w TypeScript reprezentujący wartość, która nigdy nie powinna wystąpić (np. gdy funkcja zawsze rzuca wyjątek)
- stderr - Standard Error - standardowe wyjście błędów w systemach operacyjnych, używane do wyświetlania komunikatów o błędach
- localStorage - Mechanizm przeglądarki pozwalający na przechowywanie danych w formie par klucz-wartość po stronie klienta
Linkografia do dalszej exploracji :)

Konrad Bysiek
Frontend Developer / Tech Lead