Представьте ситуацию: ваше приложение делает запрос к 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
- перехватит любое исключение, возникшее где угодно выше по коду.
| Метод | Ловит ошибки в .then() | Ловит ошибки в самом обработчике | Рекомендация |
|---|---|---|---|
| .then(null, errFn) | Да | Нет | Не рекомендуется |
| .catch(errFn) | Да | Да | Золотой стандарт |
| try...catch (с async/await) | Да | Да | Лучший выбор для читаемости |
Стратегии восстановления и повторный выброс
Одна из крутых фишек .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() может «исправить» ошибку и вернуть успешный результат.