Menu
Русский

try/catch в JavaScript: обработка ошибок на практике

Разбираем try/catch/finally в JavaScript: как ловить ошибки, что внутри объекта error, когда пробрасывать исключение дальше и где try/catch — не лучший выбор.

try/catch в JavaScript — это подстраховка, а не страховка

Когда строка JavaScript-кода выбрасывает ошибку, выполнение мгновенно останавливается, и ошибка начинает «всплывать» вверх по стеку вызовов. Если её никто не перехватит, программа упадёт (в Node) или нарисует красную простыню в консоли (в браузере). Конструкция try/catch как раз и нужна, чтобы перехватить этот момент — мол, «я знаю, что здесь может рвануть, вот что делать вместо падения».

Базовая форма такая:

index.js
Output
Click Run to see the output here.

JSON.parse выбрасывает SyntaxError. Выполнение сразу перескакивает в блок catch, а сама ошибка попадает в переменную err. Третий console.log всё равно отработает — падение удалось локализовать.

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

Объект ошибки в JavaScript

То, что было выброшено, привязывается к параметру, указанному в catch (...). Обычно это экземпляр Error с тремя полезными полями:

index.js
Output
Click Run to see the output here.

name подскажет, какой это подкласс ошибки (TypeError, RangeError, SyntaxError и так далее — о них подробнее в следующей статье). message — это читаемое описание. А stack — полный стек вызовов, штука просто незаменимая при отладке.

Один нюанс: в JavaScript через throw можно бросить вообще что угодно, не только объекты Error. В старом коде иногда встречается что-то вроде throw "что-то сломалось". Когда пишете свой throw, всегда бросайте именно Error — тогда у вызывающего кода будет стек вызовов:

index.js
Output
Click Run to see the output here.

Блок finally выполняется в любом случае

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

index.js
Output
Click Run to see the output here.

Спиннер прячется независимо от того, успешной была загрузка или нет. Без finally пришлось бы дублировать эту строку в обеих ветках — и в одной из них неизбежно забыть.

finally срабатывает даже тогда, когда внутри try или catch есть return. Функция вернёт значение после того, как отработает finally. Иногда это удивляет, но чаще всего это именно то поведение, которое нужно.

catch нужен не всегда

Блок catch необязателен. Конструкция try/finally без catch вполне рабочая и бывает полезна, когда нужно гарантированно выполнить очистку, а саму ошибку обрабатывать не хочется — пусть летит дальше:

index.js
Output
Click Run to see the output here.

Внутренний try/finally освобождает блокировку, даже если fn() выбросит ошибку, но при этом не глотает её — вызывающий код всё равно её увидит. Молчаливое проглатывание ошибок («что-то упало, но я никому не сказал») — один из самых страшных кошмаров при отладке.

Проброс ошибки: что-то обрабатываем, остальное пускаем дальше

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

index.js
Output
Click Run to see the output here.

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

try catch с async/await в JavaScript

Внутри async-функции отклонённый await-промис превращается в обычное исключение, и try/catch ловит его точно так же, как синхронные ошибки:

index.js
Output
Click Run to see the output here.

Тонкий момент: промис нужно обязательно await-ить внутри блока try. Если просто вернуть промис без await, то отклонение произойдёт уже после выхода из функции, и блок catch его попросту не увидит:

async function bad() {
  try {
    return fetch("/broken");  // нет await — вызывающий код увидит отказ промиса
  } catch (err) {
    // никогда не выполняется
  }
}

Правило простое: в async-функциях оборачивайте через await то, что должен поймать try/catch.

Вложенный try/catch в JavaScript

Блоки try/catch можно вкладывать друг в друга — это удобно, когда внутренний и внешний код падают по разным причинам и обрабатывать их нужно по-разному:

index.js
Output
Click Run to see the output here.

Внутренний catch разбирается с ситуацией «данные не той формы» — возвращает безопасное значение по умолчанию. Внешний ловит случай «на входе вообще не JSON», оборачивает ошибку и пробрасывает дальше. Вложенный try catch в javascript уместен, когда у каждого уровня своя стратегия восстановления. Если оба блока делают одно и то же — смело сплющивайте в один.

Когда try/catch не нужен

try/catch — это инструмент для ожидаемых, восстановимых сбоев. Это не способ замаскировать баги.

  • Не оборачивайте всё тело функции «на всякий случай». Если у вас нет конкретного плана на случай ошибки, пусть она всплывает наверх — непойманная ошибка со стектрейсом куда полезнее, чем тихо проглоченная.
  • Не используйте try/catch для управления потоком выполнения. try-блоки создают реальные накладные расходы и делают код мутнее, чем обычная проверка через if. if (user) всегда лучше, чем try { user.name } catch {}.
  • Не ловите ошибку ради «залогировал и забыл». Как минимум пробросьте её дальше или верните какое-то значение-маркер, по которому вызывающий код поймёт, что что-то пошло не так.

Мысленный тест: «а что будет делать вызывающий код, когда это упадёт?». Если ответа нет — значит, вы пока не готовы ловить эту ошибку через catch.

Краткая шпаргалка

  • try { ... } catch (err) { ... } — перехват выброшенных ошибок.
  • finally { ... } — выполняется всегда; нужен для очистки ресурсов.
  • throw new Error("...") — всегда бросайте наследников Error, чтобы стектрейс работал как надо.
  • throw err; внутри catch — проброс ошибки, когда не можете её обработать.
  • await внутри try — обязателен, иначе try/catch не увидит отказ промиса в async/await.

Дальше: типы ошибок

TypeError, RangeError, SyntaxError — в JavaScript есть целое семейство встроенных классов ошибок, и понимание того, какой из них что означает, делает обработку ошибок в javascript куда точнее. Об этом — в следующей статье.

Часто задаваемые вопросы

Как работает try/catch в JavaScript?

Потенциально опасный код кладёте в try { ... }. Если внутри что-то выбросит исключение, управление тут же перейдёт в блок catch (err) { ... }, а в err попадёт само брошенное значение. Если ошибок не было — catch просто пропускается. Ещё можно добавить finally { ... } — он выполнится в любом случае и удобен для уборки за собой (закрыть соединение, снять флаг загрузки и т.п.).

Когда реально стоит использовать try/catch?

Оборачивайте им то, что может честно упасть в рантайме: JSON.parse от непроверенных данных, работа с fetch, сетевой или файловый ввод-вывод. Не нужно заворачивать каждую строчку — если вы не знаете, что делать с ошибкой, пусть она летит выше. Огромный try/catch вокруг рабочего кода не обрабатывает баги, а прячет их.

Ловит ли try/catch асинхронные ошибки?

Только если вы пишете await перед промисом внутри try. Просто вызов somePromise() без await никуда не попадёт — получите unhandled rejection. С async/await всё работает ровно так же, как с синхронным кодом. Если же работаете с «сырыми» промисами — используйте .catch() в цепочке.

Как пробросить ошибку дальше?

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

Учитесь программировать с Coddy

НАЧАТЬ