Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Promise.all, promise.race, promise.allsettled, promise.any». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Promise.all, Promise.race, Promise.allSettled і Promise.any** - чотири статичні методи для координації кількох промісів, кожен з різною стратегією обробки успіху та помилок. ```javascript Promise.all([p1, p2, p3]); // всі або нічого Promise.race([p1, p2]); // перший завершений виграє Promise.allSettled([p1, p2, p3]); // чекає всіх, не відхиляється Promise.any([p1, p2, p3]); // перший успішний, або AggregateError ``` **Головне:** `all` і `race` короткозамикають; `allSettled` і `any` завжди обробляють всі проміси до кінця.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**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` передає це рішення назад у код, що викликає, замість того щоб приймати його самостійно.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.