ПроКодинг - Откроем для вас мир IT!

Представьте ситуацию: ваше приложение делает запрос к API, сервер внезапно падает, а ваш интерфейс просто «зависает», не сообщая пользователю, что пошло не так. В консоли браузера при этом горит пугающая красная надпись Unhandled Promise Rejection. Знакомо? Большинство разработчиков начинают с простых колбэков, но когда логика усложняется, они сталкиваются с «адом колбэков» и хаосом в обработке ошибок. Чтобы приложение не падало при каждом чихе, нужно четко понимать, чем обработка исключений в промисах отличается от старого доброго подхода с ошибками в колбэках.

Когда мы работали с обычными колбэками, стандартным правилом было передавать ошибку первым аргументом: (err, data) => { ... }. Это работало, но требовало проверки if (err) на каждом уровне вложенности. С появлением промисов правила игры изменились. Теперь ошибка не передается как аргумент, а переводит весь объект в состояние «отклонено».

Главные состояния промиса и как они ломаются

Чтобы не путаться в методах, вспомним базу. Promise - это объект, представляющий результат асинхронной операции. Он может находиться в одном из трех состояний: pending (ожидание), fulfilled (выполнено) или rejected (отклонено).

Ошибка в промисе возникает в двух случаях:

  • Вы явно вызвали функцию reject(error) внутри конструктора.
  • Внутри кода произошел «выброс» исключения через throw new Error().
Интересный момент: если вы забудете обработать такой переход в состояние rejected, JavaScript выдаст предупреждение о «необработанном отклонении». В Node.js старые версии могли просто завершить процесс, а современные браузеры просто заспамят консоль, но пользователь так и останется смотреть на бесконечный спиннер.

Почему .catch() лучше второго аргумента в .then()

Многие новички используют запись .then(onSuccess, onError), думая, что это то же самое, что и .catch(). На самом деле, между ними есть критическая разница. Второй аргумент в .then() ловит ошибки только из предыдущего звена цепочки, но он абсолютно беспомощен, если ошибка случилась в самом onSuccess.

Посмотрите на этот сценарий: если в вашем обработчике успеха произойдет опечатка в переменной или ошибка в логике, второй аргумент .then() её не заметит. А вот метод .catch() - это специализированный метод для перехвата ошибок из всех предыдущих звеньев цепочки Promise - перехватит любое исключение, возникшее где угодно выше по коду.

Сравнение способов обработки ошибок в Promise
Метод Ловит ошибки в .then() Ловит ошибки в самом обработчике Рекомендация
.then(null, errFn) Да Нет Не рекомендуется
.catch(errFn) Да Да Золотой стандарт
try...catch (с async/await) Да Да Лучший выбор для читаемости
Иллюстрация цепочки промисов с перехватом ошибки через .catch()

Стратегии восстановления и повторный выброс

Одна из крутых фишек .catch() заключается в том, что он может «исцелить» цепочку. Если вы вернете из него какое-то значение или новый промис, следующая функция .then() сработает так, будто всё прошло успешно. Это полезно, когда у вас есть «запасной вариант» (fallback). Например, если основной API недоступен, вы можете вернуть данные из локального кэша.

Но что делать, если ошибка слишком серьезная и её нельзя игнорировать на этом уровне? Здесь в игру вступает повторный выброс. Если внутри .catch() написать throw error, ошибка полетит дальше по цепочке к следующему обработчику. Это позволяет создать многоуровневую систему: локальный .catch() пытается исправить мелкую проблему, а глобальный .catch() в конце приложения просто выводит уведомление «Что-то пошло не так».

Очистка ресурсов с помощью .finally()

Есть вещи, которые должны произойти всегда, независимо от того, упал запрос или пришел с успехом. Например, скрыть индикатор загрузки или закрыть соединение с базой данных. Писать один и тот же код в .then() и в .catch() - это плохой тон и лишнее дублирование.

Для этого создан .finally() - метод, который гарантированно выполняется после завершения промиса, независимо от его итогового состояния . Он не получает аргументов и не влияет на результат цепочки, что делает его идеальным для «уборки» за собой.

Футуристический кристаллический купол, защищающий цифровой код

Переход на async/await: как не потерять ошибки

Современный JavaScript почти полностью перешел на синтаксис async/await, который делает асинхронный код похожим на синхронный. Но здесь кроется ловушка. Если вы используете await и не оборачиваете его в try...catch, ошибка приведет к тому самому Unhandled Promise Rejection.

Правильный подход выглядит так: вы оборачиваете вызов в блок try, а в catch описываете логику обработки. Это гораздо естественнее для тех, кто пришел из других языков программирования, и позволяет обрабатывать несколько асинхронных вызовов в одном блоке, не выстраивая бесконечные цепочки из .then().

Практический чеклист по обработке ошибок

Чтобы ваш код был стабильным, следуйте этим простым правилам:

  • Всегда ставьте .catch() в конце каждой цепочки промисов.
  • Используйте Promise.reject() для создания ошибок, которые должны быть обработаны асинхронно.
  • Не используйте второй аргумент .then(), если хотите ловить ошибки во всей цепочке.
  • Выносите общую логику завершения (стопперы, закрытие окон) в .finally().
  • При использовании async/await всегда используйте try...catch или возвращайте промис с прикрепленным .catch().

Что такое Unhandled Promise Rejection?

Это ситуация, когда промис перешел в состояние rejected, но в коде не было предусмотрено ни одного обработчика (например, метода .catch()), чтобы перехватить эту ошибку. Это может привести к утечкам памяти или неожиданному завершению работы приложения.

Можно ли вернуть выполнение в основной поток после ошибки в .catch()?

Да, если вы вернете из функции .catch() какое-либо значение или новый успешно завершенный промис, выполнение продолжится со следующего метода .then() в цепочке.

В чем разница между throw и reject()?

Внутри конструктора new Promise() throw приведет к отклонению промиса, но использование reject() считается более явным и правильным способом. Вне конструктора (например, в async функции) throw автоматически превращает возвращаемое значение в отклоненный промис.

Нужно ли использовать .catch() в каждой функции, которая возвращает промис?

Не обязательно. Часто лучше позволить ошибке «всплыть» до верхнего уровня, где есть один централизованный обработчик, который решит, как уведомить пользователя.

Как работает .finally() в сравнении с .catch()?

.catch() выполняется только если произошла ошибка. .finally() выполняется всегда: и при успехе, и при ошибке. При этом .finally() не может изменить результат промиса, в то время как .catch() может «исправить» ошибку и вернуть успешный результат.