Пошук уроків, статей та іншого контенту
Порівняєте послідовне й паралельне виконання Promise та використаєте all, allSettled, race і any для різних сценаріїв.
Комбінатори Promise — це методи, які об’єднують кілька асинхронних операцій і визначають, коли та з яким результатом завершиться спільна операція.
Найчастіше використовують:
Promise.all() — потрібні результати всіх операцій;
Promise.allSettled() — потрібен статус кожної операції, навіть якщо деякі завершилися помилкою;
Promise.race() — потрібен результат першої завершеної операції;
Promise.any() — потрібен результат першої успішної операції.
Перед використанням комбінаторів важливо розуміти різницю між послідовним і паралельним виконанням.
У послідовному сценарії наступна операція починається лише після завершення попередньої:
const delay = (value, milliseconds) =>
new Promise((resolve) => {
setTimeout(() => resolve(value), milliseconds);
});
async function runSequentially() {
console.time("Послідовно");
const first = await delay("Перша операція", 1000);
console.log(first);
const second = await delay("Друга операція", 1000);
console.log(second);
console.timeEnd("Послідовно");
}
runSequentially();Загальний час виконання становитиме приблизно 2 секунди, оскільки друга операція починається після першої.
Послідовне виконання потрібне, якщо:
друга операція залежить від результату першої;
операції мають виконуватися в конкретному порядку;
потрібно обмежити кількість одночасних запитів.
Якщо операції незалежні, їх можна запустити одночасно:
const delay = (value, milliseconds) =>
new Promise((resolve) => {
setTimeout(() => resolve(value), milliseconds);
});
async function runInParallel() {
console.time("Паралельно");
const firstPromise = delay("Перша операція", 1000);
const secondPromise = delay("Друга операція", 1000);
const first = await firstPromise;
const second = await secondPromise;
console.log(first);
console.log(second);
console.timeEnd("Паралельно");
}
runInParallel();Тут обидві операції запускаються майже одночасно, тому загальний час становитиме приблизно 1 секунду.
Ще зручніше використовувати Promise.all().
Promise.all() приймає ітерований об’єкт, зазвичай масив Promise, і повертає Promise, який:
виконується успішно, коли успішно завершилися всі операції;
повертає масив результатів у тому самому порядку, у якому Promise були передані;
відхиляється одразу після першої помилки.
const delay = (value, milliseconds) =>
new Promise((resolve) => {
setTimeout(() => resolve(value), milliseconds);
});
async function loadData() {
const promises = [
delay("Профіль", 1000),
delay("Повідомлення", 500),
delay("Налаштування", 800),
];
const results = await Promise.all(promises);
console.log(results);
}
loadData();Результат:
[ 'Профіль', 'Повідомлення', 'Налаштування' ]Хоча "Повідомлення" завершується першим, результати все одно розташовані в порядку Promise у вхідному масиві.
const successfulRequest = Promise.resolve("Дані отримано");
const failedRequest = Promise.reject(new Error("Сервер недоступний"));
async function loadData() {
try {
const results = await Promise.all([
successfulRequest,
failedRequest,
]);
console.log(results);
} catch (error) {
console.error("Не вдалося завантажити всі дані:", error.message);
}
}
loadData();Якщо хоча б один Promise відхилено, Promise.all() також буде відхилено.
Важливо: Promise.all() не скасовує інші операції автоматично. Якщо один запит завершився помилкою, інші запущені запити можуть продовжувати виконуватися.
Promise.all()Цей комбінатор підходить, коли всі результати потрібні для подальшої роботи:
async function loadPage() {
const [user, products, notifications] = await Promise.all([
fetchUser(),
fetchProducts(),
fetchNotifications(),
]);
return {
user,
products,
notifications,
};
}Якщо завантаження одного з ресурсів є критично важливим, Promise.all() дозволяє одразу повідомити про загальну помилку.
Promise.allSettled() чекає завершення всіх Promise — як успішних, так і відхилених.
Він завжди виконується успішно, якщо сам аргумент є коректним ітерованим об’єктом. Результатом буде масив об’єктів зі статусами:
{ status: "fulfilled", value: ... };
{ status: "rejected", reason: ... }.
const requests = [
Promise.resolve("Профіль завантажено"),
Promise.reject(new Error("Не вдалося завантажити аватар")),
Promise.resolve("Налаштування завантажено"),
];
async function loadOptionalData() {
const results = await Promise.allSettled(requests);
for (const result of results) {
if (result.status === "fulfilled") {
console.log("Успіх:", result.value);
} else {
console.log("Помилка:", result.reason.message);
}
}
}
loadOptionalData();Такий підхід корисний, коли помилка одного ресурсу не повинна зупиняти обробку інших:
завантаження кількох незалежних віджетів;
масова обробка файлів;
виконання кількох фонових операцій;
отримання необов’язкових даних.
async function getSuccessfulResults(promises) {
const results = await Promise.allSettled(promises);
return results
.filter((result) => result.status === "fulfilled")
.map((result) => result.value);
}
getSuccessfulResults([
Promise.resolve(10),
Promise.reject(new Error("Помилка")),
Promise.resolve(30),
]).then(console.log);Результат:
[ 10, 30 ]На відміну від Promise.all(), Promise.allSettled() не переривається після першої помилки.
Promise.race() повертає Promise, який завершується за результатом першого Promise, що завершився:
якщо першим завершився успішний Promise, результат буде успішним;
якщо першим завершився відхилений Promise, результат буде помилковим.
const delay = (value, milliseconds, shouldReject = false) =>
new Promise((resolve, reject) => {
setTimeout(() => {
if (shouldReject) {
reject(new Error(value));
} else {
resolve(value);
}
}, milliseconds);
});
async function runRace() {
try {
const result = await Promise.race([
delay("Повільна операція", 1500),
delay("Швидка операція", 500),
]);
console.log(result);
} catch (error) {
console.error("Перша завершена операція завершилася помилкою:", error.message);
}
}
runRace();Результат:
Швидка операціяPromise.race() часто використовують, щоб обмежити час очікування:
const delay = (milliseconds) =>
new Promise((resolve) => {
setTimeout(() => resolve("Запит завершено"), milliseconds);
});
const timeout = (milliseconds) =>
new Promise((_, reject) => {
setTimeout(() => {
reject(new Error("Час очікування вичерпано"));
}, milliseconds);
});
async function requestWithTimeout() {
try {
const result = await Promise.race([
delay(3000),
timeout(1000),
]);
console.log(result);
} catch (error) {
console.error(error.message);
}
}
requestWithTimeout();У цьому прикладі через одну секунду буде отримано помилку тайм-ауту.
Важливо: Promise.race() лише визначає результат об’єднаного Promise. Він не скасовує повільну операцію, яка продовжить виконання у фоновому режимі.
Для реального скасування операцій, які це підтримують, можна використовувати AbortController, наприклад із fetch.
Promise.any() очікує перший успішний Promise.
Він:
виконується успішно після першого fulfilled;
ігнорує помилки Promise, які завершилися раніше;
відхиляється лише тоді, коли всі Promise відхилені;
у такому випадку повертає AggregateError.
const delay = (value, milliseconds, shouldReject = false) =>
new Promise((resolve, reject) => {
setTimeout(() => {
if (shouldReject) {
reject(new Error(value));
} else {
resolve(value);
}
}, milliseconds);
});
async function getDataFromReplicas() {
try {
const result = await Promise.any([
delay("Репліка 1 недоступна", 300, true),
delay("Дані з репліки 2", 800),
delay("Дані з репліки 3", 1200),
]);
console.log(result);
} catch (error) {
if (error instanceof AggregateError) {
console.error("Усі джерела завершилися помилкою:");
for (const reason of error.errors) {
console.error(reason.message);
}
}
}
}
getDataFromReplicas();Результат:
Дані з репліки 2Promise.any() корисний, коли є кілька альтернативних джерел одного й того самого ресурсу:
кілька серверних реплік;
кілька CDN;
кілька способів отримати дані;
резервні сервіси.
race() та any()Ці методи схожі, але мають різну логіку:
Promise.race([
Promise.reject(new Error("Швидка помилка")),
delay("Повільний успіх", 1000),
]);
// Відхилення через швидку помилку
Promise.any([
Promise.reject(new Error("Швидка помилка")),
delay("Повільний успіх", 1000),
]);
// Успіх через повільний успішний Promiserace() реагує на перше завершення будь-якого типу.
any() реагує лише на перше успішне завершення.
Promise.all()Використовуйте, коли:
потрібні всі результати;
помилка будь-якої операції робить результат непридатним;
операції можна виконувати паралельно.
Promise.allSettled()Використовуйте, коли:
потрібно дочекатися всіх операцій;
окремі помилки допустимі;
потрібно отримати звіт про кожну операцію.
Promise.race()Використовуйте, коли:
потрібен перший завершений результат;
потрібно реалізувати тайм-аут;
є кілька операцій, але важлива швидкість першої відповіді.
Promise.any()Використовуйте, коли:
потрібен перший успішний результат;
помилки окремих альтернатив допустимі;
є резервні джерела даних.
Комбінатори приймають не лише Promise. Звичайні значення автоматично перетворюються на вже виконані Promise:
async function example() {
const results = await Promise.all([
10,
"текст",
Promise.resolve(true),
]);
console.log(results);
}
example();Результат:
[ 10, 'текст', true ]Це зручно, якщо масив формується динамічно і частина його елементів уже містить готові значення.
Комбінатори мають спеціальну поведінку для порожніх масивів:
Promise.all([]).then(console.log);
// []
Promise.allSettled([]).then(console.log);
// []
Promise.race([]).then(console.log);
// Promise залишається в стані pending
Promise.any([]).catch((error) => {
console.log(error instanceof AggregateError);
console.log(error.errors);
});
// true
// []Promise.race([]) ніколи не завершується, оскільки в ньому немає операції, яка могла б завершитися першою.
async/awaitКомбінатори зручно використовувати разом із async/await:
async function loadResources() {
try {
const [user, orders] = await Promise.all([
fetchUser(),
fetchOrders(),
]);
return { user, orders };
} catch (error) {
console.error("Помилка завантаження:", error.message);
throw error;
}
}Якщо потрібно обробити кожен результат окремо, краще скористатися Promise.allSettled():
async function loadWidgets() {
const results = await Promise.allSettled([
loadWeatherWidget(),
loadNewsWidget(),
loadRecommendationsWidget(),
]);
return results.map((result) => {
if (result.status === "fulfilled") {
return result.value;
}
return {
error: result.reason.message,
};
});
}await усередині mapНевдалий варіант:
async function loadUsers(ids) {
const users = ids.map(async (id) => {
return fetchUser(id);
});
return users;
}map() повертає масив Promise, а не масив готових користувачів.
Правильний варіант:
async function loadUsers(ids) {
const userPromises = ids.map((id) => fetchUser(id));
return Promise.all(userPromises);
}Або коротше:
async function loadUsers(ids) {
return Promise.all(ids.map((id) => fetchUser(id)));
}map() має бути послідовнимЯкщо операції не можна запускати паралельно, використовуйте цикл:
async function loadUsersSequentially(ids) {
const users = [];
for (const id of ids) {
const user = await fetchUser(id);
users.push(user);
}
return users;
}Не кожен масив операцій потрібно передавати до Promise.all(). Вибір залежить від залежностей між операціями та допустимого навантаження.
Уявімо, що застосунок завантажує основні й додаткові дані для сторінки:
const delay = (value, milliseconds, shouldReject = false) =>
new Promise((resolve, reject) => {
setTimeout(() => {
if (shouldReject) {
reject(new Error(`${value}: помилка завантаження`));
} else {
resolve(value);
}
}, milliseconds);
});
async function loadDashboard() {
const [user, statistics] = await Promise.all([
delay("Користувач", 500),
delay("Статистика", 700),
]);
const optionalResults = await Promise.allSettled([
delay("Рекомендації", 300, true),
delay("Останні новини", 400),
]);
const optionalData = optionalResults
.filter((result) => result.status === "fulfilled")
.map((result) => result.value);
return {
user,
statistics,
optionalData,
};
}
loadDashboard()
.then((dashboard) => {
console.log(dashboard);
})
.catch((error) => {
console.error("Критична помилка:", error.message);
});У цьому прикладі:
користувач і статистика завантажуються паралельно;
помилка рекомендацій не перешкоджає отриманню новин;
критичні дані обробляються через Promise.all();
додаткові дані обробляються через Promise.allSettled().
await для незалежних операційconst first = await loadFirst();
const second = await loadSecond();Якщо операції незалежні, це збільшує час очікування. Краще:
const [first, second] = await Promise.all([
loadFirst(),
loadSecond(),
]);Promise.all() збере всі помилкиPromise.all() повертає лише першу помилку, яка спричинила відхилення спільного Promise. Якщо потрібно отримати статус усіх операцій, використовуйте Promise.allSettled().
race() та any()race() може завершитися помилкою через перший відхилений Promise;
any() ігнорує відхилені Promise, доки не відхиляться всі.
Комбінатори не скасовують операції, які вже запущені. Вони лише визначають стан спільного Promise.
AggregateErrorЯкщо всі Promise у Promise.any() відхилені, помилка міститиме масив причин у властивості errors:
try {
await Promise.any([
Promise.reject(new Error("Помилка A")),
Promise.reject(new Error("Помилка B")),
]);
} catch (error) {
console.log(error.errors);
}Послідовне виконання підходить для залежних операцій.
Незалежні операції зазвичай варто запускати паралельно.
Promise.all() чекає всі успішні результати або завершується після першої помилки.
Promise.allSettled() чекає завершення всіх операцій і повертає статус кожної.
Promise.race() використовує перший завершений Promise, незалежно від успіху чи помилки.
Promise.any() повертає перший успішний результат і відхиляється лише після помилок усіх операцій.
Комбінатори не скасовують уже запущені асинхронні операції.
Для масиву асинхронних операцій зазвичай потрібна конструкція Promise.all(array.map(...)).