Promise.all, promise.race, promise.allsettled, promise.any
Promise.all, Promise.race, Promise.allSettled і Promise.any - чотири статичні методи конструктора Promise, кожен із яких координує групу промісів за різною стратегією завершення.
Теорія
TL;DR
Promise.allчекає, поки всі проміси виконаються; перша відмова скасовує всеPromise.raceповертає те, що завершиться першим, успішно чи з помилкоюPromise.allSettledзавжди резолвиться з результатом кожного промісу, що б не сталосяPromise.anyповертає перший успішний результат; відхиляє лише якщо всі проміси провалились, черезAggregateError- Правило вибору: потрібні всі результати?
all. Найшвидший?race. Повна картина?allSettled. Перший успішний?any
Короткий приклад
const p1 = Promise.resolve("fast");
const p2 = new Promise(r => setTimeout(() => r("medium"), 100));
const p3 = Promise.reject("boom");
Promise.all([p1, p2, p3]).catch(e => console.log(e)); // "boom"
Promise.race([p1, p2, p3]).then(console.log); // "fast"
Promise.allSettled([p1, p2, p3]).then(console.log);
// [{status:"fulfilled",value:"fast"},{status:"fulfilled",value:"medium"},{status:"rejected",reason:"boom"}]
Promise.any([p3, p1, p2]).then(console.log); // "fast"p1 вже резолвлений. p3 відхиляється одразу. Promise.all зупиняється на p3. Promise.race не доходить до p3, бо p1 вже переміг. Promise.allSettled збирає всі три результати. Promise.any пропускає відмову і повертає перший успішний.
Головна різниця
all і race короткозамикають: all зупиняється на першій відмові, race - на першому завершенні будь-якого роду. allSettled і any продовжують до кінця. allSettled збирає все підряд; any зупиняється, щойно знаходить успіх. Саме тут ховається більшість багів у продакшен-коді з async.
Коли що використовувати
- Всі результати обов'язкові для продовження:
Promise.all(паралельні API-запити, завантаження конфігів) - Таймаут:
Promise.raceіз проміс-таймером, який відхиляє через N мс - Часткові помилки допустимі:
Promise.allSettled(дашборди, batch-збереження, логування) - Перший доступний виграє:
Promise.any(CDN failover, кілька серверів) - Комбінований патерн:
Promise.anyвсерединіPromise.raceдодає глобальний таймаут до логіки failover
Таблиця порівняння
| Метод | Короткозамикає? | Завершується коли | При успіху | При помилці | Додано в |
|---|---|---|---|---|---|
Promise.all | Так, перша відмова | Всі виконались АБО один відхилив | Array значень (в порядку вводу) | Причина першої відмови | ES2015 |
Promise.race | Так, перше завершення | Будь-який проміс завершився | Перше завершене значення | Причина першої відмови | ES2015 |
Promise.allSettled | Ні | Всі проміси завершились | Array об'єктів {status, value/reason} | Ніколи не відхиляється | ES2020 |
Promise.any | Так, перший успіх | Перший виконався АБО всі відхилились | Перше успішне значення | AggregateError з усіма причинами | ES2021 |
Як це працює під капотом
V8 реалізує всі чотири методи як нативні C++ функції всередині PromiseConstructor. Promise.all ітерує вхідний масив, зберігає результати за індексом і тригерить відмову в момент, коли будь-який проміс відхиляється, не чекаючи інших. Promise.allSettled використовує той самий лічильник, але збільшує його і для fulfilled, і для rejected, тому ніколи не замикається достроково. Promise.race і Promise.any встановлюють прапорець переможця при першому відповідному завершенні. Не-проміс значення автоматично огортаються в Promise.resolve(), тому Promise.race([1, slowPromise]) резолвиться з 1 негайно.
Типові помилки
Очікування, що Promise.all збере всі помилки
// Неправильно: приходить тільки перша відмова
Promise.all([failA(), failB()]).catch(e => {
console.log(e); // Одна помилка. Друга втрачена.
});
// Правильно: використовуй allSettled і фільтруй
const results = await Promise.allSettled([failA(), failB()]);
const errors = results
.filter(r => r.status === "rejected")
.map(r => r.reason);Використання Promise.race без таймаута - зависання
// Неправильно: якщо жоден проміс не завершується, виклик зависає назавжди
Promise.race([slowPromise, anotherSlowPromise]).then(handle);
// Правильно: додай проміс-таймаут
const withTimeout = (p, ms) => Promise.race([
p,
new Promise((_, reject) =>
setTimeout(() => reject(new Error("timeout")), ms)
)
]);Ігнорування AggregateError від Promise.any
// Неправильно: e.message загальне, причини кожної відмови втрачено
Promise.any([fail1, fail2]).catch(e => console.log(e.message));
// Правильно: перевіряй e.errors
Promise.any([fail1, fail2]).catch(e => {
if (e instanceof AggregateError) {
console.log(e.errors); // масив усіх причин відмови
}
});Непроміс-значення у вхідному масиві
Promise.race([1, slowPromise]).then(console.log); // 1, негайноЦе спрацює, бо 1 автоматично огортається в Promise.resolve(1). Але ніякого наміру це не виражає. Тримай всі елементи масиву реальними промісами.
Мутація результатів allSettled напряму
// Заплутано: мутуємо об'єкти результатів на місці
results.forEach(r => { r.value = r.value?.toUpperCase(); });
// Чисто: маппуємо в нові об'єкти
const updated = results.map(r =>
r.status === "fulfilled" ? { ...r, value: r.value.toUpperCase() } : r
);Де зустрічається в реальних проєктах
- React Query використовує
Promise.allвнутрішньо дляuseQueriesпри паралельних запитах - Next.js
getStaticPropsзапускає fetcher-функції паралельно черезPromise.all - SWR викликає
Promise.allSettledпри оновленні кількох ключів кешу черезmutate - Express middleware використовує
Promise.raceдля таймаутів на зовнішні виклики dns.promisesв Node.js для batch-запитів покладається наPromise.all
Follow-up питання
Q: Що станеться, якщо передати порожній масив у кожен метод?
A: Promise.all([]) і Promise.allSettled([]) одразу резолвляться з []. Promise.race([]) не завершується ніколи. Promise.any([]) одразу відхиляється з AggregateError.
Q: Promise.race([p1, p2]), де p1 відхиляється першим. Що відбувається з p2?
A: race одразу відхиляється з причиною p1. p2 продовжує виконуватись у фоні, але його результат ігнорується. Ланцюжок вже пішов далі.
Q: Чому Promise.any кидає AggregateError, а не останню причину відмови?
A: Коли всі вхідні проміси провалились, немає жодного "останнього" що мав би пріоритет. AggregateError збирає всі причини в e.errors, нічого не втрачаючи. Доступ до окремих причин: e.errors[0], e.errors[1] тощо.
Q: Як написати поліфіл для Promise.any під середовища без ES2021?
A: Інвертуй логіку Promise.all. Зберігай відмови в масиві та рахуй виконані. Резолви при першому успіху. Якщо всі відхилились - кидай new AggregateError(rejectionsArray, "All promises were rejected").
Q: Які наслідки для пам'яті при allSettled проти all на тисячах промісів?
A: allSettled тримає всі об'єкти результатів у пам'яті до завершення кожного промісу. all може звільнити посилання раніше при відмові. На великих масштабах це має значення; розбивай великі batch-и на чанки незалежно від методу.
Приклади
Паралельне завантаження даних через Promise.all
async function fetchUserDashboard(userId) {
// Всі три запити виходять одночасно
const [user, posts, comments] = await Promise.all([
fetch(`/api/users/${userId}`).then(r => r.json()),
fetch(`/api/posts?userId=${userId}`).then(r => r.json()),
fetch(`/api/comments?userId=${userId}`).then(r => r.json())
]);
return { user, posts, comments };
}Якщо будь-який з трьох ендпоінтів впаде, вся функція кине помилку. Саме так і треба: показувати профіль із відсутніми даними гірше, ніж показати сторінку помилки.
Таймаут через Promise.race
const withTimeout = (promise, ms) =>
Promise.race([
promise,
new Promise((_, reject) =>
setTimeout(() => reject(new Error(`Timed out after ${ms}ms`)), ms)
)
]);
app.get("/data", async (req, res) => {
try {
const data = await withTimeout(fetchFromDatabase(), 3000);
res.json(data);
} catch (err) {
res.status(503).json({ error: err.message });
}
});Цей патерн варто додавати до кожного Express-роуту, що звертається до зовнішнього сервісу. Без нього один повільний залежний сервіс може тримати запит відкритим годинами.
CDN failover через Promise.any
async function loadAsset(assetName) {
const cdnUrls = [
`https://cdn1.example.com/${assetName}`,
`https://cdn2.example.com/${assetName}`,
`https://cdn3.example.com/${assetName}`
];
try {
return await Promise.any(
cdnUrls.map(url =>
fetch(url).then(r => {
if (!r.ok) throw new Error(`${url}: ${r.status}`);
return url;
})
)
);
} catch (err) {
// err.errors містить причину кожної окремої відмови
throw new Error("Всі CDN-вузли недоступні");
}
}Часткова деградація через Promise.allSettled
async function loadDashboard() {
const [userResult, statsResult, feedResult] = await Promise.allSettled([
fetchUser(), // Обов'язково
fetchStats(), // Опціонально
fetchActivityFeed() // Опціонально
]);
if (userResult.status === "rejected") {
throw new Error("Неможливо відрендерити дашборд без даних користувача");
}
return {
user: userResult.value,
stats: statsResult.status === "fulfilled" ? statsResult.value : null,
feed: feedResult.status === "fulfilled" ? feedResult.value : []
};
}Провал stats і feed - прийнятний. Провал user - ні. allSettled передає це рішення назад у код, що викликає, замість того щоб приймати його самостійно.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.