try/catch to siatka asekuracyjna, a nie pas bezpieczeństwa
Gdy linia kodu JavaScript rzuca błąd, wykonanie natychmiast się zatrzymuje, a błąd wędruje w górę stosu wywołań. Jeśli nic go nie złapie, program się wysypuje (w Node) albo zalewa konsolę czerwonym tekstem (w przeglądarce). try/catch pozwala to przechwycić, czyli powiedzieć: „wiem, że to może się nie udać, i oto co chcę zrobić w zamian”.
Podstawowa postać:
JSON.parse rzuca SyntaxError. Wykonanie od razu przeskakuje do bloku catch, a błąd trafia do err. Trzecie console.log nadal się wykonuje: awaria została opanowana.
Jeśli blok try wykona się bez rzucenia błędu, blok catch jest całkowicie pomijany. Służy tylko na wypadek niepowodzenia.
Obiekt błędu
Cokolwiek zostanie rzucone, trafia do parametru w catch (...). Zwykle jest to instancja Error z trzema przydatnymi polami:
name mówi, jaka to podklasa (TypeError, RangeError, SyntaxError itd., więcej o nich w następnym dokumencie). message to opis czytelny dla człowieka. stack to pełny ślad stosu, który przy debugowaniu jest na wagę złota.
Jedna pułapka: JavaScript pozwala rzucić cokolwiek, nie tylko obiekty Error. Stary kod czasem robi throw "something broke". Gdy piszesz własne throw, zawsze rzucaj Error, żeby wywołujący dostali stack trace:
finally wykonuje się zawsze
finally to opcjonalny trzeci blok, który wykonuje się niezależnie od tego, czy błąd został rzucony i czy catch go obsłużył. Służy do sprzątania: zamykania plików, zwalniania blokad, ukrywania spinnerów ładowania:
Spinner znika niezależnie od tego, czy ładowanie się udało. Bez finally trzeba by napisać tę linię w obu gałęziach i w jednej z nich o niej zapomnieć.
finally wykonuje się nawet wtedy, gdy blok try albo catch zawiera return. Funkcja zwraca wartość po wykonaniu finally. Czasem to zaskakuje, ale zwykle jest dokładnie tym, czego chcesz.
catch nie zawsze jest potrzebny
catch jest opcjonalny. Samo try/finally jest poprawne i przydatne, gdy chcesz mieć gwarantowane sprzątanie, ale nie zamierzasz obsługiwać błędu, tylko przepuścić go dalej:
Wewnętrzne try/finally zwalnia blokadę nawet wtedy, gdy fn() rzuca błąd, ale go nie połyka: wywołujący nadal go widzi. Ciche połykanie błędów („coś padło i nikomu nie powiedziałem”) to jeden z najczęstszych koszmarów przy debugowaniu.
Ponowne rzucanie: obsłuż część, resztę przekaż wyżej
Blok catch nie musi obsługiwać wszystkiego. Możesz sprawdzić błąd, obsłużyć to, co ma sens, a resztę rzucić ponownie:
Sprawdzenie przez instanceof to właściwy wzorzec: rozpoznaj błędy, z których umiesz się wydostać, a wszystko inne przepuść w górę stosu. Połykanie każdego błędu pustym blokiem catch to zły znak: tracisz wszelką informację, gdy wydarzy się coś nieoczekiwanego.
try/catch z async/await
W funkcji async odrzucone promise'y, na które czekasz przez await, zamieniają się w rzucone błędy, a try/catch obsługuje je tak samo jak błędy synchroniczne:
Jeden niuans: musisz użyć await na promise wewnątrz bloku try. Jeśli zwrócisz promise bez await, odrzucenie nastąpi, gdy funkcja już się zakończy, i catch nigdy go nie zobaczy:
async function bad() {
try {
return fetch("/broken"); // no await - caller sees the rejection
} catch (err) {
// never runs
}
}
Praktyczna zasada: w funkcjach async używaj await na tym, co ma objąć try/catch.
Zagnieżdżone try/catch
Bloki try/catch możesz zagnieżdżać, gdy kod wewnętrzny i zewnętrzny zawodzą z różnych powodów, które chcesz obsłużyć inaczej:
Wewnętrzny catch obsługuje sytuację „dane mają zły kształt”, zwracając bezpieczną wartość domyślną. Zewnętrzny obsługuje „dane wejściowe w ogóle nie były JSON-em”, opakowując błąd i rzucając go ponownie. Zagnieżdżanie jest w porządku, gdy każda warstwa ma własną strategię odzyskiwania. Jeśli oba bloki robiłyby to samo, spłaszcz je.
Kiedy nie używać try/catch
try/catch to narzędzie do spodziewanych, możliwych do obsłużenia niepowodzeń. Nie służy do zamiatania błędów pod dywan.
- Nie opakowuj całego ciała funkcji „na wszelki wypadek”. Jeśli nie masz realnego planu na błąd, pozwól mu wypłynąć: nieobsłużony błąd ze stack trace jest bardziej przydatny niż cichy.
- Nie używaj go do sterowania przepływem. Bloki
trymają realny narzut i zaciemniają kod w porównaniu ze sprawdzeniemif.if (user)jest lepsze niżtry { user.name } catch {}. - Nie łap błędów tylko po to, żeby je zalogować i zignorować. Przynajmniej rzuć je ponownie albo zwróć wartość specjalną, którą wywołujący może wykryć.
Test myślowy: „co robi użytkownik tego kodu, gdy to się nie uda?”. Jeśli nie znasz odpowiedzi, nie jest to jeszcze moment na łapanie tego błędu.
Ściągawka
try { ... } catch (err) { ... }: przechwytywanie rzuconych błędów.finally { ... }: wykonuje się zawsze, używaj do sprzątania.throw new Error("..."): zawsze rzucaj podklasyError, żeby stack trace działał.throw err;wewnątrzcatch: rzuć ponownie, gdy nie umiesz obsłużyć błędu.awaitwewnątrztry: niezbędne, żebytry/catchwidział odrzucenia asynchroniczne.
Dalej: typy błędów
TypeError, RangeError, SyntaxError: JavaScript ma całą rodzinę wbudowanych klas błędów, a wiedza, co która oznacza, sprawia, że łapanie i zgłaszanie błędów staje się dużo precyzyjniejsze. O tym jest następny dokument.
Najczęściej zadawane pytania
Jak działa try/catch w JavaScript?
Umieść ryzykowny kod w try { ... }. Jeśli cokolwiek w środku rzuci błąd, wykonanie od razu przeskakuje do bloku catch (err) { ... }, a rzucona wartość trafia do err. Jeśli nic nie zostanie rzucone, blok catch jest pomijany. Opcjonalny blok finally { ... } wykonuje się w obu przypadkach, co przydaje się do sprzątania.
Kiedy używać try/catch w JavaScript?
Używaj go wokół operacji, które realnie mogą się nie udać w czasie działania: JSON.parse na niezaufanych danych, odpowiedzi fetch, operacje na plikach albo w sieci. Nie opakowuj każdej linii: jeśli nie masz planu na odzyskanie sprawności, pozwól błędowi wypłynąć wyżej. Szerokie try/catch wokół działającego kodu ukrywa błędy zamiast je obsługiwać.
Czy try/catch łapie błędy asynchroniczne?
Tylko wtedy, gdy użyjesz await na promise wewnątrz bloku try. Samo wywołanie somePromise() nie zostanie złapane: błąd stanie się nieobsłużonym odrzuceniem (unhandled rejection). Z async/await try/catch działa tak samo jak w kodzie synchronicznym. Przy zwykłych promise'ach użyj zamiast tego .catch() w łańcuchu.
Jak ponownie rzucić błąd w JavaScript?
Wewnątrz catch po prostu napisz throw err; (albo rzuć nowy błąd, który go opakowuje). Przydaje się to, gdy chcesz obsłużyć część błędów, a resztę przepuścić dalej: najpierw sprawdź typ albo komunikat błędu, obsłuż to, co umiesz, a resztę rzuć ponownie, żeby wywołujący wyżej nadal je widzieli.