Promise в JavaScript: цепочки, ошибки и объединение операций
Автор: Казачкин Даниил Михайлович · Обновлено
Promise представляет будущий результат операции и позволяет связать продолжение с её успехом или ошибкой. Он помогает описать зависимости между действиями, но сам по себе не…
Promise представляет будущий результат операции и позволяет связать продолжение с её успехом или ошибкой. Он помогает описать зависимости между действиями, но сам по себе не создаёт отдельный поток и не отменяет уже начатую работу.
Среда и наблюдаемый порядок
Первый блок сохраните как promises.mjs и запустите в Node.js 22 или новее. Он также работает в браузерной консоли. Здесь нет внешнего сервера: источник результата создаётся внутри примера, поэтому порядок не зависит от сети. Для урока нужны функции, callback и представление об ошибках. Синтаксис async/await пока не используется, чтобы поведение цепочек оставалось явным.
У Promise есть состояния ожидания, успешного завершения и отклонения. После окончательного результата состояние не переключается обратно. Однако вызов resolve с другим Promise может связать результат с ещё не завершённой операцией; слова «resolved» и «fulfilled» не во всех ситуациях взаимозаменяемы. Для прикладной логики важнее понимать, когда доступно значение и какая цепочка отвечает за обработку отказа.
Полный пример: преобразование и восстановление
console.log('start');
const source = new Promise((resolve) => {
console.log('executor');
resolve(6);
});
source
.then((value) => value * 2)
.then((value) => {
if (value > 10) throw new RangeError('limit');
return value;
})
.catch((error) => {
console.log(error.name);
return 10;
})
.then((value) => console.log('result', value))
.finally(() => console.log('finished'));
console.log('end');Вывод: start, executor, end, RangeError, result 10, finished. Функция, переданная конструктору, выполняется синхронно. Обработчики then выполняются позже, даже когда исходный Promise уже получил результат. Исключение внутри второго обработчика превращается в отклонение следующего Promise. catch возвращает обычное значение и тем самым восстанавливает успешное продолжение.
Из этого следует важная практическая граница: тяжёлый расчёт внутри конструктора всё равно блокирует текущий поток. Обёртка new Promise не превращает синхронный алгоритм в фоновый. Если API уже возвращает Promise, дополнительный конструктор обычно не нужен. Он оправдан, например, при аккуратном преобразовании callback-интерфейса, где вы действительно связываете сигнал завершения с resolve или reject.
Каждое then создаёт новое продолжение
Возврат обычного значения из обработчика становится успешным результатом следующего звена. Возврат Promise заставляет цепочку дождаться его результата. Исключение даёт отклонение. Если ничего не вернуть, результатом будет undefined. Эти четыре варианта позволяют читать цепочку без догадок о скрытом состоянии внешних переменных.
Частая ошибка — начать асинхронную операцию внутри then, но не вернуть её. Внешняя цепочка считает обработчик завершённым раньше внутренней работы, а её ошибка может остаться без владельца. В результате следующий шаг читает пустой список или показывает успех до фактического сохранения. Возвращайте Promise связанной операции и переносите обработку её результата в следующее звено, если вложенность не нужна для ограничения области ошибки.
Два вызова source.then(...) создают две ветви. Они не становятся последовательностью только потому, что строки стоят рядом. Если второе действие зависит от результата первого, свяжите их через возвращаемый Promise. Это особенно важно для сохранения и последующего обновления интерфейса: оба действия могут пользоваться одним источником, но требовать разного порядка.
Ошибка: восстановить или передать дальше
catch может вернуть запасное значение, а может выбросить ошибку дальше. Первый выбор означает, что операция теперь считается успешно обработанной. Если просто вывести ошибку в консоль и ничего не вернуть, следующая стадия получит успешный undefined. Для обязательных данных такое «восстановление» часто создаёт вторичную ошибку вдали от причины.
finally подходит для очистки или снятия индикатора ожидания независимо от результата. Возвращённое обычное значение не заменяет полезный результат цепочки. Но исключение или отклонённый Promise из finally способны изменить итог на ошибку. Поэтому очистка также должна иметь понятный контракт: например, не скрывать исходный сбой необязательной диагностической операцией.
Обрабатывать нужно цепочку, которую вы реально создали. Наличие одного catch на исходном Promise не автоматически покрывает ошибки всех отдельно созданных ветвей. Назначайте владельца каждой операции: вызывающий код ожидает результат либо код явно обрабатывает завершение самостоятельно. Намеренно запущенная фоновая задача всё равно требует понятного способа наблюдать её отказ.
all и allSettled решают разные задачи
Promise.all подходит, когда для результата нужны все успешные значения. Он сохраняет порядок входов и отклоняется при отказе одного участника; остальные уже начатые операции при этом не отменяются. Promise.allSettled ждёт исход каждого участника и возвращает записи со статусом. Это удобно для отчёта о независимых проверках, где один отказ не делает результаты остальных бесполезными.
Передача функций вместо вызовов не запускает их автоматически. Сначала решите, когда операции должны стартовать, затем передайте полученные Promise в объединение. Для большого списка отдельный вопрос — ограничение одновременной работы: Promise.all не является ограничителем нагрузки. Запустить тысячи запросов и затем ждать их вместе — не то же самое, что обрабатывать их по несколько за раз.
Для выбора первого результата есть ещё два инструмента. Promise.race принимает первый окончательный исход — успех или отказ. Promise.any ждёт первый успех, пропуская отдельные отказы; если отклонены все участники, результатом будет отклонение с AggregateError. У пустого race нет победителя, поэтому Promise остаётся ожидающим, а пустой any отклоняется. Это разные контракты: первая завершившаяся проверка может завершиться неуспешно.
Ни один из этих способов объединения не отменяет проигравшие операции. Например, таймер в race может сообщить, что ожидание превысило срок, но запрос продолжит работу, если отдельно не поддерживает отмену. Не путайте прекращение ожидания результата с освобождением ресурса, который его вычисляет.
Практика: отчёт о независимых проверках
Нужно сохранить порядок двух проверок и отобразить как успешный результат, так и причину отказа. Следующий пример самостоятельный: он моделирует оба исхода без случайных задержек. Перед запуском решите, почему Promise.all с одним общим catch не даёт такой же полной структуры отчёта.
const checks = [
Promise.resolve('syntax ok'),
Promise.reject(new Error('missing export')),
];
Promise.allSettled(checks).then((results) => {
const report = results.map((result, index) => ({
check: index + 1,
message: result.status === 'fulfilled'
? result.value
: result.reason.message,
}));
console.log(JSON.stringify(report));
});Ожидается [{"check":1,"message":"syntax ok"},{"check":2,"message":"missing export"}]. В упражнении причина отказа заведомо Error; для произвольного внешнего API она может быть другим значением, и форматирование следует защитить проверкой. Обработка частичного успеха должна быть осознанной частью интерфейса, а не случайным превращением всех ошибок в строки.
Частые вопросы
Почему resolve не завершил исполнение конструктора?
Это вызов функции, а не return. Код после него продолжает выполняться. Изменить уже выбранный результат повторным вызовом нельзя, но побочные действия всё ещё могут произойти.
Можно ли отменить Promise?
Общего метода отмены у Promise нет. Отмена принадлежит выполняемой операции, например запросу с AbortSignal. Отклонение одной обёртки не гарантирует остановки работы внутри.
Почему результат появляется после end?
Реакции Promise ставятся в очередь микрозадач. Точный разбор очередей и их влияния на отрисовку вынесен в урок об event loop; здесь достаточно отделять синхронный запуск от асинхронного продолжения.
Связанные исследования
- Почему Promise опережает таймер: проверяем модель выполнения JavaScript — Причинная модель очередности Promise и таймеров: границы ECMAScript и HTML, контролируемые трассы, готовность реакции и ограничения выводов о времени исполнения.