Strona główna / Artykuły / Dlaczego `catch ()` wywołuje błąd SyntaxError w JavaScript?

Dlaczego `catch ()` wywołuje błąd SyntaxError w JavaScript?

Dowiedz się, dlaczego pusta lista parametrów catch całkowicie zakłóca analizę kodu w JavaScript, i zobacz dwa poprawne pod względem gramatyki sposoby napisania bloku catch bez parametrów.

1836 słów

Na pierwszy rzut oka wydaje się, że ten fragment wyświetla „Error”. W rzeczywistości cały plik zawiera błąd SyntaxError, ponieważ zapis catch () bez niczego pomiędzy nawiasami nigdy nie był poprawnym JavaScriptem.

Załóżmy rozmowę kwalifikacyjną na stanowisko front-end w drugiej rundzie. To, co wydawało się prostym pytaniem wprowadzającym, okazało się trudniejsze. Rozmówca poprosił o przykład struktury try-catch, która rzuci błąd i zapisze komunikat w bloku catch, a następnie dodał wskazówkę: „Nie potrzebujesz obiektu błędu.”

Jeśli nie potrzebujesz obiektu błędu, naturalnym krokiem jest pominięcie jego deklaracji. Lata pisania kodu w formie function handler() {} uczą nas, by po prostu pozostawić listę parametrów pustą:

try {
  throw "Error";
} catch () {
  console.log('Error')
}

Gdy zapytano, co to wyświetli, oczywistą odpowiedzią wydaje się być "Error" – wyrzucana jest ta strona, wykonuje się blok catch, a następnie uruchamia się console.log. Ale gdy rozmówca faktycznie uruchomił kod:

Uncaught SyntaxError: Unexpected token ')'

Nic nie zostaje wydrukowane. Ani „Error”, ani nic innego. Skrypt w ogóle nie wykonuje żadnego polecenia. Wtedy rozmówca wypowiedział zdanie, od którego pochodzi tytuł tego artykułu:

Masz pięć lat doświadczenia w JavaScript i nie wiesz, jak działa blok try-catch.

To nie było sformułowane jako pytanie. To było stwierdzenie, i to nieprzyjemne, ponieważ okazało się prawdziwe. Przez pięć lat pracy nad JavaScript tym programiście ani razu nie wpisano catch (). Parametr zawsze miał nazwę, nawet w przypadkach, gdy nigdy tak naprawdę nie był używany. Pierwsza próba jego pominięcia ujawniła zasadę, której po prostu nigdy nie nauczył się.

catch ma dokładnie dwa dopuszczalne formy

Oficjalna gramatyka klauzuli catch (ECMA-262, rozdział 14.15, The try Statement) jest zwięzła:

Catch :
  catch ( CatchParameter ) Block
  catch Block
CatchParameter :
  BindingIdentifier
  BindingPattern

Przyjrzyj się uważnie pierwszej regule. Gdy występują nawiasy, obowiązkowy jest CatchParameter w ich wnętrzu. W gramatyce nic nie wskazuje, że jest on opcjonalny. Musi odpowiadać dokładnie jednemu powiązaniu: zwykłemu identyfikatorowi, takiemu jak e, lub wzorcowi dekonstrukcji, takiemu jak {message}. Puste nawiasy nie pasują do żadnej z tych reguł.

Druga reguła, wprowadzona w ES2019, całkowicie usuwa nawiasy. Puste nawiasy nie pasują do żadnej z reguł. Dlatego gdy parser odczytuje catch (, oczekuje następnego ważnego tokenu powiązania, zamiast tego napotyka ) i przerywa obróbkę. To dokładnie to сообщение, które wyświetla V8: Unexpected token ')'.

To rodzi uzasadnione pytanie: dlaczego function f() {} kompiluje się bez problemów, podczas gdy catch () {} nigdy tak nie robi? Lista parametrów funkcji podlega gramatyce FormalParameters, która dopuszcza zerową liczbę parametrów, wartości domyślne oraz składnię typu rest. CatchParameter został celowo zaprojektowany inaczej – reprezentuje dokładnie jeden obowiązkowy parametr, bez listy oddzielonej przecinkami, bez wartości domyślnych i bez elementu typu rest. Catch nie rozumie pojęcia pustej listy parametrów, więc szybka metoda „po prostu opróżnić nawiasy”, która działa w przypadku funkcji, nie ma tu zastosowania.

Błąd ten pojawia się przed uruchomieniem kodu

W tym błędzie kryło się jeszcze jedno nieporozumienie: traktowanie błędów wyłącznie jako zjawiska występującego w czasie wykonywania. Ten konkretny błąd pojawia się podczas parsingu, zanim w ogóle rozpocznie się wykonywanie skryptu. Silnik przeszukuje cały skrypt, nie może go dopasować do gramatyki i zgłasza błąd SyntaxError, zanim pojedynczy wiersz zdąży zostać wy wykonany. Wszystko inne w tym pliku zostaje przez to wprowadzone w błąd.

Dodanie jeszcze jednego wiersza uwyraźnia tę kwestię. Potwierdzono to w Chrome:

console.log('before');            // NEVER runs
try { throw "Error"; } catch() { }
Uncaught SyntaxError: Unexpected token ')'

Wiersz console.log('before') znajduje się powyżej błędnie sformułowanej klauzuli catch i sam w sobie nie ma żadnych problemów, a mimo to nic nie jest wyświetlane. Cały skrypt nie udaje się skompilować, więc nic z niego nie działa.

To prowadzi do wniosku, który na początku łatwo przeoczyć: blok try-catch wewnątrz pliku nie może przechwycić błędu SyntaxError pochodzącego z tego samego pliku. Aby blok catch mógł zostać uruchomiony, kod otaczający musiałby już zostać pomyślnie zinterpretowany, co właśnie się nie udało. Jedynym sposobem na przechwycenie tego typu błędu jest użycie oddzielnej jednostki kompilacyjnej poprzez eval, new Function lub dynamiczne import().

Dwa sposoby poprawnego zapisu

Oba poniższe podejścia zostały sprawdzone w Chrome i oba są poprawne.

Opcja 1: zachować parametr, po prostu go nie używać. Deklaracja parametru catch, do którego nigdy się nie odwołujemy, jest w pełni dopuszczalna i zawsze tak była we wszystkich wersjach ECMAScript.

try {
  throw "Error";
} catch (e) {
  console.log('caught, e unused');
}
// logs: caught, e unused

Opcja 2: usunąć całkowicie nawiasy. To jest składnia opcjonalnego przypisywania w ES2019: bez nawiasów, bez parametru, po prostu catch {.

try {
  throw "Error";
} catch {
  console.log('caught without binding');
}
// logs: caught without binding

Model myślowy, który należy pamiętać: skracanie catch (e) oznacza usunięcie nawiasów, a nie usunięcie tego, co się w nich znajduje. Dokładnie pomiędzy tymi dwoma poprawnymi formami pojawia się błąd SyntaxError.

Pochodzenie składni catch bez nawiasów

Opcjonalne przypisywanie w catch powstało jako propozycja TC39 autorstwa Michaela Ficarry. Doszła do etapu 4 w styczniu 2018 roku i stała się częścią ES2019. Wkrótce potem pojawiło się wsparcie w przeglądarkach i środowiskach wykonawczych: Chrome 66, Firefox 58, Safari 11.1 oraz Node.js 10 – wszystkie one je obsługują, co oznacza, że można je bezpiecznie używać w praktycznie każdym środowisku, które jest dziś dostępne.

Uzasadnienie tej propozycji odpowiada dokładnie temu scenariuszowi, który sprawił problemy kandydatowi na rozmowę: kodu, w którym złapany błąd rzeczywiście nigdy nie jest potrzebny. Pomyślmy o sprawdzaniu, czy ciąg znaków może zostać przetworzony jako prawidłowy JSON, i przechodzeniu na wartość domyślną w przypadku niepowodzenia, albo o wzorcach wykrywania funkcji, gdzie samo złapanie błędu dostarcza wszystkich potrzebnych informacji. W takich przypadkach nadanie błędowi nazwy e powoduje jedynie utworzenie zmiennej, która jest przypisywana, ale nigdy nie jest odczytywana – a sama propozycja wskazuje, że taki wzorzec zwykle sygnalizuje błąd w innym miejscu kodu.

Warto być precyzyjnym co do tego, co faktycznie zmieniła ta propozycja: wprowadziła nową strukturę składniową dla bloku catch bez żadnej listy parametrów. Nie zalegalizowała pustych nawiasów – one nie zostają opróżnione, lecz całkowicie usunięte. Dzięki temu zachowana zostaje zasada, że CatchParameter, jeśli występuje, musi reprezentować dokładnie jedno powiązanie, a także catch { staje się wizualnie spójny z finally {, który od początku nie miał nawiasów.

Dlaczego nawet doświadczeni programiści dają się na to nabrać

Istnieją trzy odrębne przyczyny, dla których ludzie na to wpadają, a żadna z nich nie wynika z braku wysiłku lub nauki.

Twoje instynkty w tym przypadku wynikają z innego, bardziej znajomego wzorca — i wprowadzają cię w błąd. Każda sygnatura funkcji, jaką kiedykolwiek napisałeś, wzmacnia przekonanie, że lista nieużytych parametrów może zostać zredukowana do pustych nawiasów: function () {} jest zupełnie normalne. Ponieważ klauzula catch wizualnie przypomina nagłówek funkcji, stosowanie tego samego skrótu wydaje się naturalne. Jednak klauzula catch nie przyjmuje listy parametrów — przyjmuje pojedyncze powiązanie — a gramatyka po prostu nigdy nie zawierała jej pustej wersji.

To błąd zazwyczaj nie ujawnia się aż do chwili, gdy próbujemy usunąć nieużywaną zmienną e. Często jest to spowodowane ostrzeżeniem narzędzia lintera. Zasada no-unused-vars w ESLint zawiera opcję caughtErrors, a od wersji 9 ta opcja ma domyślnie wartość "all" — co oznacza, że nieużywany blok catch (e) automatycznie powoduje błąd lintera. Aby naprawić to ostrzeżenie, programiści instynktownie opróżniają nawiasy w taki sam sposób, jak robią to przy sygnaturze funkcji, i właśnie w tym momencie parser im przeszkadza. Prawidłowa naprawa — całkowite usunięcie nawiasów i napisanie catch { — jest taką, do której prawie nikt nie ucieka się jako pierwszej, ponieważ w żadnym innym języku JavaScript „skracanie” nie oznacza usuwania, a jedynie opróżniania.

Błąd ten nigdy nie przetrwa na tyle długo, by pozostawić ślad. Subtelny błąd logiczny może trafić do produkcji i nękać bazę kodu przez miesiące, stając się historią z czasów rozwoju oprogramowania, którą programiści powtarzają przez lata. Ten przypadek wcale taki nie jest: to błąd SyntaxError wykryty od razu podczas analizy. Widzisz krzywą linię pod tekstem, poprawiasz go w kilka sekund i idziesz dalej bez żadnych dalszych rozważań. Nie pozostaje po nim żaden trwały ślad — i właśnie dlatego jest doskonałym pytaniem na rozmowę kwalifikacyjną. Sprawdza granicę pomiędzy składnią, którą naprawdę opanowałeś, a tą, której jedynie sądzisz, że rozumiesz.

Jedna uwaga przed tym, jak zaczniesz przepisywać każdy catch (e), jaki znajdziesz: catch { } daje Ci możliwość pominięcia nadawania nazwy błędowi, ale nie pominięcia jego obsługi. Forma ES2019 istnieje w celu realizacji uzasadnionej logiki sprawdzania i fallbacku, gdzie sam akt przechwytywania stanowi przydatny sygnał. Pusty blok catch, który po cichu pochłania rzeczywisty błąd, jest równie problematyczny jak zawsze, niezależnie od użycia nawiasów.

Odpowiedź, która lepiej by pasowała

Odpowiedź, która pozwoliłaby utrzymać to wywiad w właściwym kierunku: „Podczas parsowania pojawia się błąd SyntaxError – konkretnie Unexpected token ')'. Nic w pliku w ogóle się nie uruchamia, nawet kod znajdujący się przed blokiem try. Klauzula catch ma tylko dwa dopuszczalne formy: catch (binding) { }, lub, od ES2019, bezparametrową formę catch { }. Puste nawiasy nie pasują do żadnego z tych wzorów.”

Kluczowe punkty, które warto zapamiętać:

  • catch () z pustymi nawiasami zawsze stanowił i nadal stanowi błąd SyntaxError we wszystkich wersjach JavaScript.
  • Istnieją dokładnie dwa ważne formy: catch (e) { } z jednym nazwanym parametrem, lub catch { } bez żadnych nawiasów, wprowadzona w ES2019.
  • CatchParameter to pojedyncze, obowiązkowe powiązanie, a nie lista parametrów — nawyk używania „pustych nawiasów” z składni funkcji po prostu się nie przenosi.
  • Ponieważ jest to błąd z czasu analizy, cały plik nie może zostać uruchomiony — blok try w innym miejscu tego samego pliku nigdy nie może go przechwycić.
  • Zostawienie catch (e) z niewykorzystanym e jest technicznie dozwolone, ale ESLint 9 flagą no-unused-vars sygnalizuje to domyślnie; prawidłowym rozwiązaniem jest usunięcie nawiasów, a nie wyczyszczenie tego, co się w nich znajduje.
  • Bezparametrowy catch { } istnieje w sytuacjach, gdy wartość błędu jest rzeczywiście niepotrzebna — nie stanowi to ogólnej wymówki do milczącego odrzucania rzeczywistych błędów.
  • Czy surowa reakcja rozmówcy była uzasadniona? Tylko częściowo. Zachowanie bloku try-catch podczas wykonywania kodu nigdy nie było kwestionowane — ta część była czymś oczywistym. To, co rzeczywiście nigdy nie zostało zbadane, to sama składnia, po prostu dlatego, że codzienny kod roboczy rzadko zmusza nas do konfrontacji z nią. Obecnie sytuacja się odwróciła: usuwanie nieużywanego elementu e oznacza teraz usuwanie razem z nim nawiasów, a nie ich pustienie.

    Gdy skracasz klauzulę catch, usuń nawiasy — nigdy ich tylko nie pustuj.

    Literatura pokrewna

  • React Components 101: Budowanie użytecznych i łatwych w zarządzaniu elementów interfejsu — Dowiedz się, dlaczego dzielenie interfejsów na małe komponenty React poprawia ich użyteczność, czytelność oraz współpracę zespołu, a następnie stwórz swój pierwszy funkcjonalny komponent.