Чаму `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, яка выйшла з таго ж файла. Ёнколі блак цяпленае мае запрацаваць, адгукуючы код вялікі ўжо мусіць быць правераны з успехам, што якраз і не выйшла. Едыны спосаб падхапіць такія адмошчанні — з абсалютна разлучной единіцы кампілявання через eval, new Function або дынамічны import().
Два спосабы правільнага напісання
Оба падходы нижэй былі перакананы ў Chrome, і оба яўляюцца правільнымі.
Варыянт 1: заставіць параметр, проста не выкарыстоўваць яго. Акларацыя параметра цяпленае, якога вы ніколі не аднасвайце, ўсё ж такі ўсё правільна і завжды была такой у кожнай версіі 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 значыць выдаленне часоўніка, а не вычысцэнне таго, што ў яму знаходзіцца. Середняя форма межы гэтых двух дзейнасных варыянтав — гэта сама прычына падачы памылкі SyntaxError.
Дзе з’явіўся синтаксіс catch без часоўніка
Ідея неабяжнае прыўязвання catch пачалася як праект TC39, створанный Майклам Фікарра. У састоўе яго была 4-я стадія ў стылік 2018 года, і ён стаў часткаю ES2019. Падтрымкі з боку прыгледачаў і средавыняў выканання прыйшлі быстра: Chrome 66, Firefox 58, Safari 11.1 і Node.js 10 усе яго падтрымліваюць, таму ў прыбліжна будзь-якай сучасной средаве ўсё можна яго выкарыстоўваць.
Прычыны, якія лежачы за гэтым працоўнам прапановам, адпавядаюць самэй ситуацыі, якая створыла проблемы кандыдату на абавесці: код, у яком зловленыя памылкі насправды ніколі не ўпотрэбні. Уявіце сабе перакананне, чы рэчка паводзіцца як правільны JSON, і пераход на стандартны варыянт, калі гэта не так, або шаблоны выканання функцый, дзе проста зловленне памылкі дае вам усю неабходную інфармацыю. У такіх случаях называнне памылкі e проста стварае зменную, якой надаёцца значэнне, але якая ніколі не чытваецца — і сама працоўная пропозыцыя паказывае, што такі падход зазвычай сигналізуе пра баг у іншай частыне коду.
Цікава тое, калі будзем точны ў тым, што насправды змяніла даная працэваўка: яна увела новы спосаб стварэння граматікі для блока catch без жаднага списку параметраў. Яна не легалізавала порожнія складанкі. Складанкі не стаюць порожнімі — яны проста выкарыстоўваюцца цэлкам. Чыгуннае гэта падтрымвае правіло, што CatchParameter, калі ён прысутны, павінен выступаць толькі адной прыўязкою, і гэта робіць catch { візуальна адпоўнаймым з finally {, блакам, які ўжо з самага пачатку не меў складанак.
Чаму нават апытныя разработчыкі паддаюцца гэтаму
Існуе тры окремыя прычыны, чырвоныя людзей уводзяць у памылку, і ні адна з яных не стосуецца браку старанняў чы выучэння.
Вашы інстынкты тут выходзяць з іншага, болей звычайнага шаблона — і яны вас падводзяць. Кожная сігнатура функцыі, якую вы калі-небудзь пісалі, падтрымляе ідэю, што список невыкарыстоўваных параметраў можа быць зменшаны да порожніх складоў: function () {} — гэта абсалютна нормальна. Паколькі клаза catch візуальна належыць да заголовка функцыі, вжыванне таго ж скорачэння здаецца прыродным. Але клаза catch не прыймае список параметраў — яна прыймае адну зв’язку — і граматыка проста ніколі не включала яе порожню версію.
Гэтыя памылкі зазвычай не стаюць виднымі аж да таго моменту, калі вы прымаецеся адключыць невыкарыстоўваны e. Часта гэты момент спрычыняе скарга лінтара. Правілу ESLint no-unused-vars прыналежыць опцыя 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, якія включаюць функцыі для зберагчэння дадзеных, адсрочкі выканання задач, обработкі клікаў і запрашэння дадзеных, і якія пазбавляюць новыя проекты від паўтаральнага коду.