Пошук уроків, статей та іншого контенту
Ознайомитеся з Promise.all, Promise.allSettled та AggregateError для роботи з помилками паралельних асинхронних операцій.
Паралельні асинхронні операції запускаються приблизно в один момент, а програма очікує на їхні результати разом. Це корисно, коли операції незалежні одна від одної:
завантаження кількох ресурсів;
запити до різних API;
читання кількох файлів;
виконання незалежних обчислень;
отримання даних для різних частин сторінки.
const first = fetch("/api/users");
const second = fetch("/api/products");
const third = fetch("/api/orders");
const [usersResponse, productsResponse, ordersResponse] =
await Promise.all([first, second, third]);У цьому прикладі всі три запити запускаються до очікування їхніх результатів. Послідовний варіант був би повільнішим:
const usersResponse = await fetch("/api/users");
const productsResponse = await fetch("/api/products");
const ordersResponse = await fetch("/api/orders");Послідовне виконання потрібне лише тоді, коли наступна операція залежить від результату попередньої.
Promise.all приймає ітерований об’єкт, зазвичай масив Promise, і повертає один Promise.
Він:
виконує всі операції паралельно;
виконується успішно, якщо успішними були всі операції;
повертає результати в тому самому порядку, що й вхідний масив;
відхиляється, щойно одна з операцій відхилилася.
const results = await Promise.all([
Promise.resolve("first"),
Promise.resolve("second"),
Promise.resolve("third"),
]);
console.log(results);
// ["first", "second", "third"]Порядок результатів не залежить від фактичного порядку завершення операцій:
const results = await Promise.all([
new Promise((resolve) => setTimeout(() => resolve("повільний"), 300)),
new Promise((resolve) => setTimeout(() => resolve("швидкий"), 50)),
]);
console.log(results);
// ["повільний", "швидкий"]Другий Promise завершився раніше, але його результат залишився на другій позиції.
try {
const results = await Promise.all([
Promise.resolve("успішно"),
Promise.reject(new Error("Операція завершилася помилкою")),
Promise.resolve("ще один результат"),
]);
console.log(results);
} catch (error) {
console.error(error.message);
}У разі помилки Promise.all не повертає частковий масив результатів. Блок catch отримує причину першої зафіксованої помилки.
Важливо: Promise.all не скасовує інші операції автоматично. Якщо вони вже запущені, вони можуть продовжити виконання:
const slowOperation = new Promise((resolve) => {
setTimeout(() => {
console.log("Повільна операція завершилася");
resolve("результат");
}, 500);
});
const failingOperation = new Promise((_, reject) => {
setTimeout(() => {
reject(new Error("Швидка помилка"));
}, 50);
});
try {
await Promise.all([slowOperation, failingOperation]);
} catch (error) {
console.error(error.message);
}
// Приблизно через 500 мс:
// Повільна операція завершиласяPromise.all припиняє очікувати на всі результати після помилки, але не зупиняє самі операції.
Promise.allSettled також запускає операції паралельно, але очікує завершення кожної з них — незалежно від успіху чи помилки.
Він завжди виконується успішно, якщо сам аргумент є коректним ітерованим об’єктом. Результатом є масив описів стану кожної операції.
Для успішного Promise:
{
status: "fulfilled",
value: результат
}Для відхиленого Promise:
{
status: "rejected",
reason: помилка
}Приклад:
const results = await Promise.allSettled([
Promise.resolve("дані користувача"),
Promise.reject(new Error("Сервіс тимчасово недоступний")),
Promise.resolve("дані замовлень"),
]);
for (const result of results) {
if (result.status === "fulfilled") {
console.log("Успіх:", result.value);
} else {
console.error("Помилка:", result.reason.message);
}
}Результат:
Успіх: дані користувача
Помилка: Сервіс тимчасово недоступний
Успіх: дані замовленьPromise.allSettled підходить, коли помилка однієї операції не повинна приховувати результати інших:
масове завантаження незалежних ресурсів;
надсилання повідомлень кільком отримувачам;
перевірка стану кількох сервісів;
імпорт багатьох записів;
виконання пакетних операцій із частковим успіхом.
Наприклад, можна окремо зібрати успішні результати та помилки:
const operations = [
Promise.resolve({ id: 1, name: "Іван" }),
Promise.reject(new Error("Не вдалося отримати користувача 2")),
Promise.resolve({ id: 3, name: "Олена" }),
];
const settledResults = await Promise.allSettled(operations);
const successfulValues = settledResults
.filter((result) => result.status === "fulfilled")
.map((result) => result.value);
const errors = settledResults
.filter((result) => result.status === "rejected")
.map((result) => result.reason);
console.log(successfulValues);
console.log(errors);Перевірка result.status важлива. Вона дозволяє JavaScript коректно звузити тип результату в середовищах із підтримкою статичного аналізу, наприклад у TypeScript.
Вибір залежить від вимог до операції.
усі операції є обов’язковими;
частковий результат не має сенсу;
будь-яка помилка повинна зупинити подальшу обробку;
потрібно швидко отримати спільний результат або повідомити про помилку.
операції незалежні;
потрібно отримати результат кожної операції;
допустимий частковий успіх;
усі помилки потрібно обробити або показати користувачу;
не можна втрачати успішні результати через одну помилку.
AggregateError — це тип помилки, призначений для представлення кількох помилок одночасно.
Його можна створити самостійно:
const errors = [
new Error("Помилка завантаження профілю"),
new Error("Помилка завантаження налаштувань"),
];
const aggregateError = new AggregateError(
errors,
"Не вдалося завантажити всі дані",
);
console.log(aggregateError.message);
console.log(aggregateError.errors);Властивість errors містить масив початкових помилок.
AggregateError часто використовують разом із Promise.allSettled: спочатку очікують усі операції, потім перевіряють відхилені результати та, якщо потрібно, створюють одну узагальнену помилку.
Наступна функція повертає результати, якщо всі операції успішні, і викидає AggregateError, якщо помилилася хоча б одна операція:
async function allOrAggregate(promises) {
const results = await Promise.allSettled(promises);
const errors = results
.filter((result) => result.status === "rejected")
.map((result) => result.reason);
if (errors.length > 0) {
throw new AggregateError(
errors,
`Неуспішних операцій: ${errors.length}`,
);
}
return results.map((result) => result.value);
}
async function main() {
try {
const values = await allOrAggregate([
Promise.resolve("перший результат"),
Promise.reject(new Error("Помилка другої операції")),
Promise.reject(new Error("Помилка третьої операції")),
]);
console.log(values);
} catch (error) {
if (error instanceof AggregateError) {
console.error(error.message);
for (const [index, nestedError] of error.errors.entries()) {
console.error(`Помилка ${index + 1}:`, nestedError.message);
}
} else {
console.error("Непередбачена помилка:", error);
}
}
}
main();Можливий результат:
Неуспішних операцій: 2
Помилка 1: Помилка другої операції
Помилка 2: Помилка третьої операціїЦей підхід відрізняється від Promise.all: замість першої помилки програма збирає всі помилки, які виникли під час виконання групи операцій.
Самі повідомлення помилок не завжди пояснюють, яка саме операція завершилася невдало. Корисно додавати ідентифікатор або назву операції:
async function runTask(name, task) {
try {
return await task();
} catch (error) {
throw new Error(`Операція "${name}" завершилася невдало`, {
cause: error,
});
}
}
async function runAllTasks() {
const tasks = [
runTask("завантаження профілю", async () => {
return { id: 1, name: "Іван" };
}),
runTask("завантаження замовлень", async () => {
throw new Error("Сервіс замовлень недоступний");
}),
runTask("завантаження сповіщень", async () => {
throw new Error("Тайм-аут");
}),
];
const results = await Promise.allSettled(tasks);
const errors = results
.filter((result) => result.status === "rejected")
.map((result) => result.reason);
if (errors.length > 0) {
throw new AggregateError(errors, "Помилки під час завантаження даних");
}
return results.map((result) => result.value);
}
runAllTasks().catch((error) => {
console.error(error.message);
for (const nestedError of error.errors ?? []) {
console.error(nestedError.message);
console.error("Початкова причина:", nestedError.cause?.message);
}
});Властивість cause дозволяє зберегти початкову помилку, не втрачаючи доданий контекст.
Повторну спробу варто виконувати не для кожної помилки. Наприклад:
тимчасовий збій мережі може бути повторюваним;
помилка авторизації не зникне без оновлення облікових даних;
помилка валідації не повинна повторюватися автоматично;
перевищення ліміту може вимагати затримки.
Якщо кожна операція має власну політику повторів, її краще реалізувати до передавання Promise у Promise.allSettled:
function delay(milliseconds) {
return new Promise((resolve) => {
setTimeout(resolve, milliseconds);
});
}
async function withRetry(task, attempts = 3) {
let lastError;
for (let attempt = 1; attempt <= attempts; attempt += 1) {
try {
return await task();
} catch (error) {
lastError = error;
if (attempt < attempts) {
await delay(attempt * 100);
}
}
}
throw lastError;
}
async function main() {
const operations = [
withRetry(async () => {
return "Профіль завантажено";
}),
withRetry(async () => {
throw new Error("Сервіс недоступний");
}, 2),
];
const results = await Promise.allSettled(operations);
for (const result of results) {
if (result.status === "fulfilled") {
console.log(result.value);
} else {
console.error("Остаточна помилка:", result.reason.message);
}
}
}
main();Повтори не повинні приховувати остаточну помилку. Після вичерпання кількості спроб операція має перейти у стан rejected.
Якщо після першої помилки інші операції більше не потрібні, їхнє виконання потрібно скасовувати окремо. Promise.all не має вбудованого механізму скасування.
Для API, що підтримують AbortSignal, можна використати AbortController:
async function loadResources() {
const controller = new AbortController();
const { signal } = controller;
try {
const responses = await Promise.all([
fetch("/api/profile", { signal }),
fetch("/api/orders", { signal }),
fetch("/api/notifications", { signal }),
]);
return responses;
} catch (error) {
controller.abort();
throw error;
}
}Такий код має обмеження: якщо помилка виникла в одному запиті, інші запити могли вже завершитися або майже завершитися. Крім того, конкретна асинхронна операція повинна підтримувати AbortSignal.
Скасування і обробка помилок — різні поняття:
обробка помилки визначає, що робити після невдачі;
скасування намагається припинити непотрібну роботу.
Promise.allSettled лише повідомляє про технічний стан Promise. Рішення про те, чи є частковий успіх прийнятним, належить логіці застосунку.
Наприклад, для масового імпорту можна повернути звіт:
async function importRecords(records, saveRecord) {
const results = await Promise.allSettled(
records.map((record) => saveRecord(record)),
);
return results.map((result, index) => {
if (result.status === "fulfilled") {
return {
index,
status: "success",
value: result.value,
};
}
return {
index,
status: "failure",
error: result.reason,
};
});
}
async function saveRecord(record) {
if (!record.name) {
throw new Error("Відсутнє ім’я");
}
return {
id: record.id,
saved: true,
};
}
const report = await importRecords(
[
{ id: 1, name: "Іван" },
{ id: 2 },
{ id: 3, name: "Олена" },
],
saveRecord,
);
console.log(report);Користувач у такому випадку може побачити:
скільки записів імпортовано;
які записи не вдалося зберегти;
причину кожної помилки;
можливість повторити лише невдалі операції.
Promise.any також працює з кількома Promise, але має іншу семантику:
виконується успішно після першої успішної операції;
відхиляється через AggregateError, лише якщо всі операції завершилися помилкою;
у AggregateError.errors містяться помилки всіх невдалих операцій.
try {
const value = await Promise.any([
Promise.reject(new Error("Сервер A недоступний")),
Promise.resolve("Відповідь від сервера B"),
Promise.resolve("Відповідь від сервера C"),
]);
console.log(value);
} catch (error) {
if (error instanceof AggregateError) {
console.error("Усі спроби завершилися невдало");
console.error(error.errors);
}
}Порівняння:
Promise.all — потрібні всі успішні результати;
Promise.allSettled — потрібні результати всіх операцій, включно з помилками;
Promise.any — достатньо першого успішного результату;
Promise.race — важливий перший завершений Promise, незалежно від його результату.
forEach з async-функцієюforEach не очікує Promise, який повертає його callback:
// Неправильний підхід
items.forEach(async (item) => {
await processItem(item);
});
console.log("Цей рядок може виконатися раніше");Для паралельного виконання використовуйте Promise.all:
await Promise.all(
items.map((item) => processItem(item)),
);Для послідовного виконання використовуйте цикл for...of:
for (const item of items) {
await processItem(item);
}Цей код виконує операції послідовно:
const first = await loadFirst();
const second = await loadSecond();
const third = await loadThird();Якщо операції незалежні, створіть їх до очікування:
const firstPromise = loadFirst();
const secondPromise = loadSecond();
const thirdPromise = loadThird();
const [first, second, third] = await Promise.all([
firstPromise,
secondPromise,
thirdPromise,
]);Promise.all не повертає частково заповнений масив після помилки. Якщо потрібно отримати і успішні значення, і помилки, використовуйте Promise.allSettled.
Відхилення Promise.all не означає автоматичного скасування вже запущених запитів або таймерів. Для скасування потрібен окремий механізм, наприклад AbortController, якщо його підтримує API.
messageТекст повідомлення може змінюватися та не завжди надійно ідентифікує тип помилки:
if (error.message === "Network error") {
// Логіка залежить від нестабільного тексту
}Краще використовувати власні класи помилок, коди або перевірку типу:
if (error instanceof AggregateError) {
for (const nestedError of error.errors) {
console.error(nestedError);
}
}Під час обгортання помилки не варто замінювати її контекст без збереження оригіналу:
throw new Error("Не вдалося виконати операцію", {
cause: error,
});Початкова причина допомагає під час журналювання та діагностики.
Promise.all очікує успішного завершення всіх операцій і відхиляється після першої помилки.
Порядок результатів Promise.all відповідає порядку вхідних Promise, а не порядку їхнього завершення.
Promise.all не скасовує інші операції після помилки.
Promise.allSettled очікує завершення всіх операцій і повертає об’єкти зі статусами fulfilled або rejected.
AggregateError дає змогу об’єднати кілька помилок в одну.
Комбінація Promise.allSettled і AggregateError підходить для збору всіх помилок паралельної групи.
Promise.any автоматично використовує AggregateError, коли всі спроби завершилися невдало.
Повторні спроби, скасування та правила часткового успіху потрібно проєктувати окремо від механізму очікування Promise.
Для незалежних операцій створюйте всі Promise до await, щоб не перетворити паралельне виконання на послідовне.