try..catch: перехват ошибок

ошибки≈ 7 минут

Здесь будет встроен видео-разбор темы (5–7 минут, YouTube / RuTube / VK Видео) — тем, кто лучше воспринимает на слух, текст ниже не обязателен.

Ошибка в JavaScript — не обязательно конец программы. try..catch — это способ сказать движку: «я знаю, что вот здесь может сломаться, дай мне разобраться самому».

Разделы темы:·

Обычный скрипт выполняется как список поручений: строка за строкой. Ошибка рвёт этот список — движок останавливается ровно там, где споткнулся, и остальное не выполняется. try..catch ставит на пути ошибки сеть: всё, что упало внутри try, попадает в catch, а программа идёт дальше.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

В catch приходит объект ошибки. У него есть name — тип поломки, message — человеческое описание и stack — путь, по которому движок дошёл до беды. Текст message пишут разработчики движка, поэтому в разных браузерах он может отличаться словами; name и тип — одинаковые везде.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Скобки у catch можно не писать вовсе: catch { ... }. Так делают, когда сам объект ошибки не нужен — важен только факт падения.

try..catch работает только с ошибками времени выполнения — теми, что случаются, когда код уже запущен. Если в файле забыта скобка, движок не сможет даже разобрать текст программы и не запустит из него ни строчки: ловить будет некому, потому что сам try не успеет появиться. Правило простое — сначала код должен уметь запуститься, и только потом его можно защищать.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Второе ограничение: «неправильный ответ» — это не ошибка. Number("двенадцать") вернёт NaN, обращение к несуществующему свойству вернёт undefined, и никакого исключения не будет. catch в таких местах просто не сработает — проверять значения приходится руками.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
Тип ошибкиКогда возникает
ReferenceErrorОбращение к переменной, которой нет
TypeErrorЗначение не того типа: null.length, вызов не-функции, запись в const
SyntaxErrorСтроку не удалось разобрать — например, JSON.parse на мусоре
RangeErrorЗначение вне допустимого диапазона: new Array(-1), слишком глубокая рекурсия
ErrorБазовый тип; именно его чаще всего бросают вручную

Блок finally выполняется всегда: и когда всё прошло гладко, и когда сработал catch, и даже когда внутри try стоит return. Это место для уборки — снять флаг загрузки, остановить таймер, закрыть соединение. Уборка не должна зависеть от того, повезло нам или нет.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Обратите внимание на порядок: функция уже вычислила, что вернуть, но пока не отдала результат наружу — finally вклинивается ровно в этот момент. Поэтому «после return ничего не выполняется» — правило с исключением, и это исключение называется finally.

Тогда он перебьёт результат из try — и заодно проглотит ошибку, если она была. Формально это работает, но читателю кода приходится держать в голове слишком многое, поэтому так лучше не писать.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Ошибки бросает не только движок — их можно бросать самому оператором throw. Технически бросить разрешено что угодно, хоть строку, хоть число, но так делать не стоит: у объекта Error есть name, message и стек вызовов, а у строки — ничего. Пишите throw new Error("...") и выбирайте встроенный подтип по смыслу: TypeError для не того типа, RangeError для недопустимого значения.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Хорошее сообщение отвечает на вопрос «что именно пошло не так», а не «всё плохо». Сравните: «ошибка данных» и «возраст должен быть числом» — второе можно чинить, не заглядывая в исходники.

Когда в проекте несколько источников сбоев, удобно завести собственные классы: тогда в catch можно разбирать ошибки по типу через instanceof, а не сравнивать текст сообщений. Сейчас достаточно увидеть приём целиком; отдельная статья «Свои классы ошибок» разбирает и name, и cause, и поля с подробностями.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

try..catch — синхронный сторож. Он охраняет ровно тот отрезок времени, пока выполняются строки внутри блока. Колбэк setTimeout запускается позже, когда try давно закрыт, — поймать оттуда ошибку он уже не может, как зонт не спасает от дождя, который пойдёт завтра.

С промисами та же история. Вызов асинхронной функции без await просто возвращает промис и мгновенно идёт дальше; ошибка внутри превращается в отклонение промиса и пролетает мимо catch. Лечится это одним словом — await внутри try — или методом .catch() на самом промисе.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Проверка простая: если ошибка может случиться не сейчас, а «когда-нибудь потом», обычный try её не увидит. Нужно либо дождаться результата через await, либо ловить прямо в том месте, где код продолжится, — внутри самого колбэка.

Самый вредный catch — пустой. Он не чинит поломку, а прячет её: программа делает вид, что всё хорошо, и продолжает работать с несуществующими данными. Такой баг всплывёт через три экрана и на совсем другой строке.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Правило одно: в catch вы либо действительно справляетесь с проблемой — подставляете запасное значение, показываете сообщение, пишете в лог, — либо честно пробрасываете ошибку дальше через throw err. И ловите только то, что умеете обработать: чужая ошибка должна лететь наверх, к тому, кто про неё знает.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

  • try..catch перехватывает ошибки времени выполнения: упало внутри try — управление уходит в catch, и скрипт продолжает работать.
  • Синтаксическая ошибка в самом файле не ловится: движок не разберёт такой код и не запустит из него ничего. И «неправильный результат» вроде NaN — тоже не ошибка.
  • finally выполняется всегда: после успеха, после catch и даже перед тем, как функция отдаст значение из return.
  • throw new Error("...") бросает свой сбой со своим сообщением; для разных ситуаций удобны встроенные подтипы или собственные классы ошибок.
  • Асинхронную ошибку нужно дождаться — через await внутри try или через .catch(); а пустой catch недопустим: либо обработайте, либо пробросьте дальше.

1/3Что выведет код: try { console.log("A"); throw new Error("!"); console.log("B"); } catch { console.log("C"); } finally { console.log("D"); }

2/3Поймает ли catch ошибку здесь: try { setTimeout(function () { null.x; }, 100); } catch (err) { ... }

3/3Функция возвращает значение из try, а в finally стоит console.log. Что произойдёт?

safeParse: разбор JSON без падений

Напишите функцию safeParse(raw, fallback): она пытается разобрать строку через JSON.parse и возвращает результат, а если строка битая — возвращает fallback вместо того, чтобы уронить программу.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Весь разбор заворачиваем в try, а запасное значение возвращаем из catch. Никаких проверок строки вручную не нужно — JSON.parse сам сообщит о проблеме исключением.

jsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

Обратите внимание: здесь catch не пустой — он делает осмысленную работу, подставляет запасное значение. Если бы в проекте важно было знать о битых данных, сюда стоило бы добавить и запись в лог.

divide, который сам сообщает о проблеме

Допишите функцию divide(a, b) так, чтобы она бросала TypeError, если хотя бы один аргумент не число, и RangeError при делении на ноль. Затем вызовите её в try..catch и выведите тип и сообщение ошибки.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Проверки идут до вычисления: сначала типы, потом диапазон. Так функция падает с внятным сообщением, а не возвращает молчаливые NaN и Infinity.

jsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

Разные типы ошибок выбраны не для красоты: в catch вызывающий код сможет отличить RangeError от TypeError через instanceof и отреагировать по-разному.