Чому `catch ()` викликає SyntaxError у JavaScript
Дізнайтеся, чому порожня список параметрів catch повністю руйнує парсинг JavaScript, та побачте два граматично правильні способи написання блоку catch без параметрів.
На перший погляд здається, що цей фрагмент виводить „Error“. Насправді весь файл припиняє працювати через SyntaxError, адже запис catch () без жодних елементів між дужками ніколи не був коректним JavaScript.
Якщо вам не потрібен об’єкт помилки, логічним кроком буде пропустити його оголошення. Роки роботи з кодом на кшталт function handler() {} навчають нас просто залишати список параметрів порожнім:
try {
throw "Error";
} catch () {
console.log('Error')
}
Коли запитали, що буде виведено, очевидною відповіддю здається "Error" — виводиться цей рядок, виконується блок catch, запускається console.log. Але коли співрозмовник дійсно запустив код:
Uncaught SyntaxError: Unexpected token ')'
Нічого не виводиться. Ні "Error", ні чогось іншого. Скрипт так і не виконує жодної команди. Саме тоді співрозмовник сказав ту фразу, з якої походить назва цієї статті:
У вас п’ять років досвіду роботи з JavaScript, а ви не знаєте, як працює блок try-catch.
Це не було сформульовано як запитання. Це було твердження, причому незручне, адже воно виявилося правдивим. За п’ять років роботи з JavaScript цей розробник жодного разу не вводив catch (). Параметр завжди мав назву, навіть у випадках, коли його насправді ніколи не використовували. Перша спроба його пропустити виявила правило, якого він просто ніколи не дізнався.
catch має рівно дві допустимі форми
Формальна граматика клозу catch (ECMA-262, розділ 14.15, The try Statement) є компактною:
Catch :
catch ( CatchParameter ) Block
catch Block
CatchParameter :
BindingIdentifier
BindingPattern
Уважно подивіться на перше правило синтаксису. Коли присутні дужки, обов’язковим є параметр CatchParameter всередині них. У граматиці ніде не зазначено, що він може бути необов’язковим. Він має відповідати саме одному зв’язку: простому ідентифікатору, такому як e, або шаблону розпакування, такому як {message}. Порожні дужки не відповідають жодному з цих правил.
Друге правило, введене у ES2019, повністю видаляє дужки. Порожні дужки не підходять жодному правилу. Тож коли парсер читає catch (, він очікує наступного коректного токена зв’язку, але замість цього зустрічає ) та припиняє обробку. Саме таке повідомлення виводить V8: Unexpected token ')'.
Це піднімає цілком логічне запитання: чому function f() {} компілюється без проблем, тоді як catch () {} ніколи не компілюється? Список параметрів функції підкоряється граматиці FormalParameters, яка дозволяє використовувати нуль параметрів, значення за замовчуванням та синтаксис rest. CatchParameter був навмисно розроблений інакше – він представляє саме одне обов’язкове зв’язування, без списку, розділеного комами, без значень за замовчуванням та без елемента rest. У конструкції catch немає поняття порожнього списку параметрів, тому ментальний трюк „просто залишити дужки порожніми“, який працює для функцій, тут не застосовується.
Ця помилка виникає ще до того, як ваш код буде виконаний
У цій помилці криється ще одне хибне уявлення: сприймати помилки виключно як явище під час виконання. Ця конкретна проблема виникає під час парсингу, ще до початку виконання. Механізм аналізує весь скрипт, не може знайти його відповідності граматиці та повідомляє про SyntaxError ще до того, як будь-який рядок отримає можливість виконатися. Усе інше у цьому файлі також зазнає наслідків.
Додавання ще одного рядка робить це очевидним. Це підтверджено у Chrome:
console.log('before'); // NEVER runs
try { throw "Error"; } catch() { }
Uncaught SyntaxError: Unexpected token ')'
Рядок console.log('before') знаходиться вище некоректної клозу catch, і сам по собі у ньому немає жодних проблем, проте він так і не виводиться. Увесь скрипт не компілюється, тому ніщо з нього не виконується.
Це призводить до висновку, який спочатку легко пропустити: блок try-catch всередині файлу не може спіймати помилку SyntaxError, що виникла у тому самому файлі. Щоб будь-який блок catch запрацював, навколишній код мусив би вже бути успішно оброблений, а саме це й не вдалося. Єдиний спосіб спіймати таку категорію помилок — це зовсім інша одиниця компіляції через eval, new Function або динамічне import().
Два способи правильного написання
Обидва підходи нижче були перевірені в Chrome, і обидва є коректними.
Варіант 1: залишити параметр, просто не використовувати його. Оголошення параметра catch, до якого ви ніколи не звертаєтесь, є абсолютно законним і завжди таким було у всіх версіях ECMAScript.
try {
throw "Error";
} catch (e) {
console.log('caught, e unused');
}
// logs: caught, e unused
Варіант 2: повністю видалити дужки. Це синтаксис обов’язкового захоплення помилки ES2019: без дужок, без параметра, просто catch {.
try {
throw "Error";
} catch {
console.log('caught without binding');
}
// logs: caught without binding
Необхідно пам’ятати: скорочення catch (e) означає видалення дужок, а не очищення їхнього вмісту. Саме посередині між цими двома допустимими формами і знаходиться помилка SyntaxError.
Де виник синтаксис catch без дужок
Ідея обов’язкового захоплення помилки походить від пропозиції TC39, автором якої був Майкл Фікарра. У січні 2018 року вона досягла четвертого етапу розробки та стала частиною ES2019. Підтримка з боку браузерів та середовищ виконання з’явилась швидко: Chrome 66, Firefox 58, Safari 11.1 та Node.js 10 усі її підтримують, тож її можна безпечно використовувати практично в будь-якому середовищі сьогодні.
Причина, що лежить в основі цієї пропозиції, збігається з саме тією ситуацією, яка створила проблеми у кандидата під час співбесіди: код, у якому виявлена помилка насправді ніколи не є потрібною. Уявіть собі перевірку того, чи рядок можна обробити як коректний JSON, та переход на значення за замовчуванням у разі невдачі, або шаблони виявлення функцій, де сам факт виникнення помилки вже дає всю необхідну інформацію. У таких випадках назва помилки e лише створює змінну, якій присвоюється значення, але яку ніколи не читають — і сама пропозиція вказує на те, що такий підхід зазвичай свідчить про баг в іншій частині коду.
Варто бути точним щодо того, що саме змінилося в цій пропозиції: була введена нова граматична структура для блоку catch без жодного списку параметрів. Це не легалізувало використання порожніх дужок. Дужки не стають порожніми — їх повністю видаляють. Це зберігає правило, згідно з яким, якщо параметр CatchParameter є, він має бути лише одним, і це робить catch { візуально сумісним із finally {, блоком, який з самого початку не мав дужок.
Чому навіть досвідчені розробники піддаються цьому
Існує три окремі причини, чому це спричиняє плутанину, і жодна з них не пов’язана з браком зусиль чи вивчення.
Ваші інстинкти тут ґрунтуються на іншій, більш знайомій схемі — і вони вводять вас в оману. Кожна сигнатура функції, яку ви коли-небудь писали, підкріплює думку про те, що список невикористаних параметрів можна звести до порожніх дужок: function () {} є абсолютно нормальним. Оскільки блок catch візуально схожий на заголовок функції, використання того ж скорочення здається природним. Але блок catch не приймає список параметрів — він приймає єдине прив’язування — і граматика просто ніколи не включала його порожню версію.
Ця помилка зазвичай проявляється лише у ту саму мить, коли ви намагаєтесь видалити невикористаний e. Часто це відбувається через попередження лінтера. Правило no-unused-vars ESLint містить опцію caughtErrors, і починаючи з ESLint 9 ця опція за замовчуванням дорівнює "all" — тобто невикористаний catch (e) автоматично створює помилку лінтера. Намагаючись усунути це попередження, розробники інстинктивно спорожнюють дужки, як це робиться у сигнатурі функції, і саме тут парсер їх зупиняє. Справді правильний спосіб виправлення — повністю видалити дужки та написати catch { — майже ніхто не обирає спочатку, оскільки в інших частинах JavaScript „скорочення“ означає не спорожнення, а видалення.
Цей баг ніколи не живе достатньо довго, щоб залишити слід. Тонка логічна помилка може потрапити у продакшн та переслідувати кодову базу протягом місяців, стаючи тією історією з проекту, яку розробники розповідають роками. Цей випадок зовсім не схожий на це: це SyntaxError, який виявляється миттєво під час парсингу. Ви бачите хвилясту підкреслену лінію, виправляєте її за кілька секунд та продовжуєте роботу, навіть не замислюючись. Від цього не залишається тривалої пам’яті — саме тому це чудове запитання для співбесіди. Воно досліджує межу між синтаксисом, який ви справді засвоїли, та синтаксисом, який ви лише припускаєте, що розумієте.
Одна застереження перед тим, як почати переписувати кожен catch (e), який ви знаходите: catch { } дозволяє пропустити найменування помилки, але не пропускає обробку її. Формат ES2019 існує для легітимної логіки перевірки та резервного варіанту, де сам факт спостереження за помилкою є корисним сигналом. Порожня частина коду catch, яка мовчки ігнорує справжню помилку, є такою ж проблематичною, як і завжди, незалежно від наявності дужок.
Відповідь, яка б краще підійшла
Відповідь, яка б допомогла тримати це інтерв’ю у правильному напрямку: «Це спричиняє помилку SyntaxError під час парсингу — а саме Unexpected token ')'. Ніщо у файлі не виконується, навіть код, який знаходиться перед блоком try. Клозу catch має лише дві допустимі форми: catch (binding) { } або, з ES2019, без параметрів catch { }. Порожні дужки не відповідають жодній з цих форм».
Ключові моменти, які варто запам’ятати:
catch ()із порожніми дужками завжди був, і залишається, помилкою SyntaxError у кожній версії JavaScript.- Існує рівно дві допустимі форми:
catch (e) { }із одним іменованим параметром абоcatch { }без дужок взагалі, яка була запроваджена у ES2019.
catch (e) із невикористаним e технічно дозволено, але ESLint 9 за замовчуванням позначає це правилом no-unused-vars; правильним рішенням є видалення дужок, а не очищення їхнього вмісту.catch { } існує для ситуацій, коли значення помилки справді не потрібне — це не універсальний привід для мовчазного ігнорування справжніх помилок.Чи була різка реакція інтерв’юера справедливою? Лише частково. Функціонування конструкції try-catch ніколи не ставало під сумнівом — ця частина була само собою зрозумілою. Те, що справді ніколи не досліджувалося, — це сама граматика, просто тому що повсякденний код рідко змушує з нею стикатися. Сьогодні ситуація змінилася: щоб видалити невикористовуване e, тепер потрібно видаляти разом із ним дужки, а не просто їх спорожнювати.
Під час скорочення клозу catch видаляйте дужки — ніколи не просто їх спорожнюйте.
Пов’язана література
- Набір переосмислених користувацьких хуків для кожного нового проекту React — Дізнайтеся про підібраний набір користувацьких хуків для React, які охоплюють зберігання даних, затримку виконання операцій, обробку кліків та отримання даних, і які допомагають уникнути повторюваного базового коду в нових проектах.