Promise.all, race, any и allSettled
Здесь будет встроен видео-разбор темы (5–7 минут, YouTube / RuTube / VK Видео) — тем, кто лучше воспринимает на слух, текст ниже не обязателен.
У асинхронных задач есть приятное свойство: их можно запускать пачкой. Четыре метода — Promise.all, Promise.allSettled, Promise.race и Promise.any — сворачивают массив промисов в один, и вся разница между ними в том, чего именно они ждут.
Промис начинает работу тогда, когда его создали, а не тогда, когда его начали ждать. Если написать два await подряд, вторая задача честно постоит в очереди — и вы прождёте сумму двух ожиданий вместо большего из них.
Проверим: обе «задачи» рапортуют о старте сразу, ещё до того, как кто-то подписался на результат.
Отсюда приём: сначала создаём все промисы, потом ждём их вместе. Разбирать пачку вручную неудобно — для этого есть комбинаторы, которые берут массив промисов и возвращают один общий.
Promise.all дожидается всех и отдаёт массив результатов. Порядок в нём — как в исходном массиве, а не как по скорости: медленный элемент останется на своём месте.
Теперь плохая новость. Если хотя бы один промис отклонён, общий промис отклоняется в ту же секунду — с этой самой ошибкой, не дожидаясь остальных. Результаты тех, кто успел, просто пропадают: до них уже не добраться.
all — командный зачёт: споткнулся один, не зачли всем.
Когда каждый ответ ценен сам по себе — рассылаем десять писем, сохраняем пять файлов — терять всё из-за одного невезучего глупо. Promise.allSettled дожидается всех и не отклоняется никогда.
Вместо голых значений в массиве лежат отчёты: у успешных — status: "fulfilled" и value, у провалившихся — status: "rejected" и reason.
Ошибку тут не поймать через catch — её надо искать в результатах. Это скорее плюс: язык заставляет вас решить, что делать с каждым провалом, а не заметать их под общий catch.
Promise.race — гонка без правил: побеждает тот, кто завершился первым, и неважно, успехом или ошибкой. Остальные продолжают работать, но их исход уже никого не интересует.
Отсюда главный практический рецепт — таймаут. Ставим в одну гонку настоящую задачу и промис, который через N миллисекунд обязательно отклонится.
Комбинаторы ничего не отменяют. Проигравший запрос продолжит идти по сети, сервер честно его обработает, а результат просто никому не отдадут. Для тяжёлой выгрузки это заметная трата.
Настоящая отмена — отдельная механика: AbortController и его signal, который понимает fetch. Гонка решает, «сколько мы готовы ждать», а контроллер — «когда пора прекратить работу».
Promise.any тоже возвращает финиш первого, но ошибку финишем не считает: он ждёт первый успех и молча пропускает провалы по дороге. Классический сценарий — несколько зеркал одного файла.
Если провалились все до единого, any отклоняется особой ошибкой — AggregateError. В её свойстве errors лежит массив всех причин, в порядке исходного массива.
Все четыре различаются двумя вопросами: чего они дожидаются и что считают поводом упасть.
| Метод | Ждёт | Что отдаёт | Когда отклоняется |
|---|---|---|---|
| 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 зависает навсегда: гонка без участников не может закончиться.
- Промис стартует в момент создания: создайте всю пачку сразу, а ждать её отдайте комбинатору.
- 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 миллисекунд — отклоняется ошибкой с текстом «таймаут».
Задача — гонка между делом и будильником: кто первый, тот и определяет исход.
Успел исходный промис — race отдаст его значение, а будильник прозвонит в пустоту. Не успел — победит отклонение таймера. Помните: сама задача при этом не отменяется и продолжит выполняться в фоне; чтобы действительно прервать запрос, нужен AbortController.
Сколько писем дошло
Реализуйте countOk(list) — она принимает массив промисов и возвращает промис с числом успешно выполненных. Падать из-за неудачных нельзя: считаем всех.
Раз падать нельзя — только allSettled, а дальше обычный подсчёт по полю status.
Заодно из results легко достать и причины неудач: results.filter((r) => r.status === "rejected").map((r) => r.reason.message) — получится готовый список адресов, по которым письмо не ушло.