Skip to main content

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

Короткий приклад

javascript
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 збере всі помилки

javascript
// Неправильно: приходить тільки перша відмова 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 без таймаута - зависання

javascript
// Неправильно: якщо жоден проміс не завершується, виклик зависає назавжди 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

javascript
// Неправильно: 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); // масив усіх причин відмови } });

Непроміс-значення у вхідному масиві

javascript
Promise.race([1, slowPromise]).then(console.log); // 1, негайно

Це спрацює, бо 1 автоматично огортається в Promise.resolve(1). Але ніякого наміру це не виражає. Тримай всі елементи масиву реальними промісами.

Мутація результатів allSettled напряму

javascript
// Заплутано: мутуємо об'єкти результатів на місці 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

javascript
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

javascript
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

javascript
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

javascript
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 передає це рішення назад у код, що викликає, замість того щоб приймати його самостійно.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.

Дочитали статтю?