Почему `catch ()` вызывает ошибку SyntaxError в JavaScript
Узнайте, почему пустой список параметров catch полностью нарушает парсинг JavaScript, и ознакомьтесь с двумя грамматически корректными способами написания блока catch без параметров.
На первый взгляд кажется, что этот фрагмент выводит слово «Error». На самом деле весь файл вызывает ошибку SyntaxError, потому что запись catch () без ничего между скобками никогда не была допустимой в JavaScript.
Представьте собеседование по фронтенду на втором этапе. Вопрос, который казался простым разминочным, оказался более сложным. Специалист попросил привести пример использования конструкции try-catch, при которой происходит выброс ошибки и в блоке catch записывается сообщение, а затем дал подсказку: «вам не нужен объект ошибки».
Если объект ошибки не требуется, логично просто опустить его объявление. Годы практики написания кода вида 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: полностью убрать скобки. Это синтаксис необязательной связи catch в ES2019: без скобок, без параметра, просто catch {.
try {
throw "Error";
} catch {
console.log('caught without binding');
}
// logs: caught without binding
Необходимо помнить следующее: сокращение catch (e) означает удаление скобок, а не удаление содержимого внутри них. Равновесие между этими двумя допустимыми формами — именно там возникает ошибка SyntaxError.
Происхождение синтаксиса catch без скобок
Идея необязательной связи 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, охватывающим функции хранения данных, замедления обработки событий, обработку кликов и загрузку данных, которые помогают избавиться от повторяющегося шаблонного кода в новых проектах.