Promise.all, race, any и allSettled

асинхронность≈ 7 минут

Здесь будет встроен видео-разбор темы (5–7 минут, YouTube / RuTube / VK Видео) — тем, кто лучше воспринимает на слух, текст ниже не обязателен.

У асинхронных задач есть приятное свойство: их можно запускать пачкой. Четыре метода — Promise.all, Promise.allSettled, Promise.race и Promise.any — сворачивают массив промисов в один, и вся разница между ними в том, чего именно они ждут.

Разделы темы:·

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

Проверим: обе «задачи» рапортуют о старте сразу, ещё до того, как кто-то подписался на результат.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Отсюда приём: сначала создаём все промисы, потом ждём их вместе. Разбирать пачку вручную неудобно — для этого есть комбинаторы, которые берут массив промисов и возвращают один общий.

Promise.all дожидается всех и отдаёт массив результатов. Порядок в нём — как в исходном массиве, а не как по скорости: медленный элемент останется на своём месте.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Теперь плохая новость. Если хотя бы один промис отклонён, общий промис отклоняется в ту же секунду — с этой самой ошибкой, не дожидаясь остальных. Результаты тех, кто успел, просто пропадают: до них уже не добраться.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

all — командный зачёт: споткнулся один, не зачли всем.

Когда каждый ответ ценен сам по себе — рассылаем десять писем, сохраняем пять файлов — терять всё из-за одного невезучего глупо. Promise.allSettled дожидается всех и не отклоняется никогда.

Вместо голых значений в массиве лежат отчёты: у успешных — status: "fulfilled" и value, у провалившихся — status: "rejected" и reason.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

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

Promise.race — гонка без правил: побеждает тот, кто завершился первым, и неважно, успехом или ошибкой. Остальные продолжают работать, но их исход уже никого не интересует.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Отсюда главный практический рецепт — таймаут. Ставим в одну гонку настоящую задачу и промис, который через N миллисекунд обязательно отклонится.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Комбинаторы ничего не отменяют. Проигравший запрос продолжит идти по сети, сервер честно его обработает, а результат просто никому не отдадут. Для тяжёлой выгрузки это заметная трата.

Настоящая отмена — отдельная механика: AbortController и его signal, который понимает fetch. Гонка решает, «сколько мы готовы ждать», а контроллер — «когда пора прекратить работу».

Promise.any тоже возвращает финиш первого, но ошибку финишем не считает: он ждёт первый успех и молча пропускает провалы по дороге. Классический сценарий — несколько зеркал одного файла.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Если провалились все до единого, any отклоняется особой ошибкой — AggregateError. В её свойстве errors лежит массив всех причин, в порядке исходного массива.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Все четыре различаются двумя вопросами: чего они дожидаются и что считают поводом упасть.

МетодЖдётЧто отдаётКогда отклоняется
Promise.allвсехмассив значений в исходном порядкена первой же ошибке
Promise.allSettledвсехмассив отчётов status + value/reasonникогда
Promise.raceпервого завершившегосяего значение или ошибкуесли первым пришёл провал
Promise.anyпервого успешногоего значениетолько если упали все — AggregateError

На практике: профиль и настройки для страницы — all (без любого из них рисовать нечего). Рассылка уведомлений — allSettled и отчёт «8 из 10». Ограничение ожидания — race с таймером. Несколько равноценных источников — any.

Все четыре принимают любой перебираемый объект, а не только массив, и молча оборачивают обычные значения: Promise.all([1, Promise.resolve(2)]) даст [1, 2].

Отдельная история — пустой массив. Три метода отвечают мгновенно, а race зависает навсегда: гонка без участников не может закончиться.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

  • Промис стартует в момент создания: создайте всю пачку сразу, а ждать её отдайте комбинатору.
  • Promise.all возвращает массив значений в порядке аргументов, но первая же ошибка отклоняет весь набор.
  • Promise.allSettled не отклоняется никогда: у каждого элемента есть status и либо value, либо reason.
  • Promise.race отдаёт первый завершившийся исход — успех или ошибку; на этом строится таймаут.
  • Promise.any ждёт первый успех, а если провалились все — бросает AggregateError с массивом errors.

1/3В Promise.all([ok(1), bad(), ok(3)]) второй промис отклонён. Что произойдёт?

2/3Все промисы, переданные в Promise.any, отклонились. Что придёт в catch?

3/3В Promise.race первым через 50 мс завершился промис с ошибкой, а через 200 мс второй — успешно. Что увидим?

Таймаут для любого промиса

Напишите функцию withTimeout(promise, ms): она возвращает промис, который ведёт себя как исходный, но если тот не уложился в ms миллисекунд — отклоняется ошибкой с текстом «таймаут».

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Задача — гонка между делом и будильником: кто первый, тот и определяет исход.

jsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

Успел исходный промис — race отдаст его значение, а будильник прозвонит в пустоту. Не успел — победит отклонение таймера. Помните: сама задача при этом не отменяется и продолжит выполняться в фоне; чтобы действительно прервать запрос, нужен AbortController.

Сколько писем дошло

Реализуйте countOk(list) — она принимает массив промисов и возвращает промис с числом успешно выполненных. Падать из-за неудачных нельзя: считаем всех.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Раз падать нельзя — только allSettled, а дальше обычный подсчёт по полю status.

jsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

Заодно из results легко достать и причины неудач: results.filter((r) => r.status === "rejected").map((r) => r.reason.message) — получится готовый список адресов, по которым письмо не ушло.