Ошибки в промисах
Здесь будет встроен видео-разбор темы (5–7 минут, YouTube / RuTube / VK Видео) — тем, кто лучше воспринимает на слух, текст ниже не обязателен.
Промис не бросает исключение так, как это делает обычный код: он молча переходит в состояние отклонения и ждёт обработчика. Найдётся catch — ошибка обработана, не найдётся — она утечёт в тишину.
Цепочка промисов ведёт себя как эстафета: как только одно звено отклонилось, все следующие then пропускаются, и управление прыгает к ближайшему catch. Поэтому обработчик обычно ставят один — в самом конце.
Неважно, на каком звене случилась беда — первом или пятом. Ошибка «проваливается» вниз мимо всех успешных обработчиков и попадает в catch целой, вместе со своим сообщением.
Внутри then можно писать обычный throw — промис сам поймает исключение и превратит его в отклонение. Это удобно: проверки данных выглядят как в синхронном коде, а результат уезжает в catch.
У then есть и второй аргумент — обработчик отклонения. Он ловит ошибку предыдущего звена, но не свою собственную: если бросить исключение в первом аргументе, соседний обработчик его не увидит. Это главная причина, почему catch в конце надёжнее.
Для цепочки — да. Оба способа переводят промис в отклонённое состояние с одной и той же причиной. Разница только в удобстве: throw короче и работает внутри условий, а Promise.reject пригодится там, где нужно вернуть значение выражением.
catch — не конец цепочки, а такое же звено. Он возвращает промис, и после него цепочка продолжается уже как успешная. Это удобный способ «починить» ошибку и подставить запасное значение.
Обратная сторона: catch видит только то, что стоит выше него. Всё, что случится ниже — включая ошибку в самом обработчике, — потребует ещё одного catch. Поэтому финальный обработчик почти всегда идёт последним.
Если у отклонённого промиса так и не появился обработчик, ошибка никуда не денется — но и не остановит программу. Код спокойно идёт дальше, а браузер чуть позже сам сообщит о проблеме в консоль. Опасность именно в этом: приложение выглядит рабочим, хотя половина данных не загрузилась.
Последняя строка в панели — заслуга песочницы учебника: она слушает событие, о котором речь чуть ниже, и переносит сообщение к вам в вывод. На настоящей странице этой строки на экране нет: браузер пишет Uncaught (in promise) только в консоль инструментов разработчика, и пользователь о сбое не узнает.
На такие случаи есть глобальное событие unhandledrejection. Его ставят на весь сайт, чтобы отправлять пропущенные ошибки в систему логирования. Это диагностика, а не лечение: событие говорит, что где-то забыли catch. Вызов event.preventDefault() в обработчике отменяет стандартную реакцию браузера — ту самую строку в консоли: ошибку уже отправили куда надо, дублировать её незачем.
Строк в панели всё равно две, и первой успела песочница: она подписалась на unhandledrejection раньше вашего кода, а preventDefault() отменяет только реакцию браузера — чужих слушателей он не останавливает. В консоли настоящей страницы от этой ошибки осталась бы одна строка — «никто не обработал: нет токена».
Правило простое: у каждой цепочки есть хозяин — catch в конце или await внутри try. Всё остальное однажды утечёт в тишину.
Ничего не меняется, меняется только запись: отклонённый промис внутри await превращается в обычное исключение, и его ловит привычный try...catch. Забытый try даёт ровно то же необработанное отклонение.
finally срабатывает при любом исходе и не получает аргументов — он не знает, успех был или ошибка. Его задача — уборка: выключить спиннер, разблокировать кнопку, закрыть соединение.
Главное свойство finally — прозрачность: он пропускает дальше исходный результат нетронутым. Значение остаётся значением, ошибка остаётся ошибкой и всё равно доезжает до catch, даже если finally стоит раньше него.
| Звено | Когда сработает | Что получает | Что уходит дальше |
|---|---|---|---|
| then(onOk) | только при успехе | значение | то, что вернул обработчик |
| then(onOk, onErr) | при успехе или ошибке выше | значение или ошибку | результат сработавшего обработчика |
| catch(onErr) | при любой ошибке выше | ошибку | успех со значением из обработчика |
| finally(fn) | всегда | ничего | исходное значение или ошибку без изменений |
- Отклонение пропускает все следующие then и прыгает к ближайшему catch — одного обработчика в конце обычно достаточно.
- throw внутри then — обычный способ отклонить промис: исключение автоматически превращается в ошибку цепочки.
- Второй аргумент then ловит ошибку предыдущего звена, но не свою собственную; отдельный catch надёжнее.
- catch возвращает промис, поэтому после него цепочка продолжается как успешная — удобно подставлять запасное значение.
- Цепочка без catch даёт unhandledrejection: страница не падает, но ошибка остаётся необработанной. finally выполняется всегда и ничего не меняет.
1/3Что выведет этот код? Promise.reject(new Error("A")).then(() => console.log("B")).catch((e) => console.log(e.message));
2/3У then есть второй аргумент — обработчик ошибки. Поймает ли он исключение, брошенное в первом аргументе того же then?
3/3Что окажется в консоли: Promise.resolve(1).finally(() => 99).then((v) => console.log(v));
Запасное значение вместо ошибки
Функция getSettings при неудаче отклоняется. Собери цепочку так, чтобы в консоль всегда печаталась тема: при успехе — полученная, при ошибке — light. Ожидаемый вывод: сначала dark, потом light.
Ошибку перехватывает catch, который возвращает объект с запасной темой. После него цепочка снова считается успешной, поэтому один общий then печатает результат обоих сценариев.
Скобки вокруг { theme: "light" } обязательны: без них стрелочная функция примет фигурные скобки за тело.
Почему catch не сработал
Автор поставил обработчик, но он не срабатывает: строки «поймали: …» в выводе нет, вместо неё — Uncaught (in promise) с той самой ошибкой. Найди причину и почини цепочку так, чтобы вывелось «поймали: не удалось удвоить 2».
catch видит только звенья, которые стоят выше него. Здесь он оказался раньше проблемного then, поэтому ошибке уже некуда было упасть. Достаточно поменять звенья местами.
Заодно обрати внимание: значение 2 дошло до обработчика, потому что catch в исходной версии просто пропустил успешный результат дальше.