Пошук уроків, статей та іншого контенту
Навчитеся будувати ланцюжки then, повертати значення між кроками й обробляти помилки через catch та finally.
Promise описує результат асинхронної операції. Він може перебувати в одному з трьох станів:
pending — очікує результату;
fulfilled — успішно завершений;
rejected — завершений з помилкою.
Методи .then(), .catch() і .finally() повертають новий Promise. Саме тому їх можна об’єднувати в ланцюжок.
Promise.resolve(2)
.then((value) => value * 3)
.then((value) => value + 1)
.then((value) => {
console.log(value); // 7
});У цьому прикладі:
Promise.resolve(2) створює вже виконаний Promise зі значенням 2.
Перший then отримує 2 і повертає 6.
Другий then отримує 6 і повертає 7.
Третій then отримує 7 і виводить його в консоль.
Кожен наступний обробник отримує значення, повернуте попереднім обробником.
Щоб передати результат далі по ланцюжку, його потрібно явно повернути через return.
function getUser() {
return Promise.resolve({
id: 42,
name: "Olena"
});
}
getUser()
.then((user) => {
console.log(`Користувач: ${user.name}`);
return user.id;
})
.then((userId) => {
console.log(`Ідентифікатор: ${userId}`);
return `user-${userId}`;
})
.then((key) => {
console.log(`Ключ: ${key}`);
});Результат:
Користувач: Olena
Ідентифікатор: 42
Ключ: user-42Якщо в обробнику then немає return, наступний обробник отримає undefined.
Promise.resolve(10)
.then((value) => {
value * 2; // Значення не повертається
})
.then((value) => {
console.log(value); // undefined
});Правильний варіант:
Promise.resolve(10)
.then((value) => {
return value * 2;
})
.then((value) => {
console.log(value); // 20
});Для коротких стрілкових функцій можна використовувати неявне повернення:
Promise.resolve(10)
.then((value) => value * 2)
.then((value) => console.log(value)); // 20thenОбробник then може повернути не лише звичайне значення, а й інший Promise. У такому випадку наступний крок дочекається його завершення.
function wait(milliseconds) {
return new Promise((resolve) => {
setTimeout(resolve, milliseconds);
});
}
console.log("Початок");
wait(1000)
.then(() => {
console.log("Минуло 1 секунда");
return wait(1000);
})
.then(() => {
console.log("Минуло ще 1 секунда");
});Перший then повертає Promise, який завершується через одну секунду. Наступний then буде виконано лише після його завершення.
Цей механізм дає змогу послідовно виконувати залежні асинхронні операції:
function loadUser() {
return Promise.resolve({ id: 7, name: "Andrii" });
}
function loadOrders(userId) {
return Promise.resolve([
{ id: 101, userId, total: 500 },
{ id: 102, userId, total: 800 }
]);
}
loadUser()
.then((user) => {
console.log(`Замовлення користувача ${user.name}`);
return loadOrders(user.id);
})
.then((orders) => {
const total = orders.reduce((sum, order) => sum + order.total, 0);
console.log(`Загальна сума: ${total}`);
});Якщо замість return loadOrders(user.id) просто викликати loadOrders(user.id), наступний обробник не чекатиме на цей Promise:
loadUser()
.then((user) => {
loadOrders(user.id); // Promise втрачено
})
.then((orders) => {
console.log(orders); // undefined
});catchМетод .catch() обробляє відхилений Promise. Помилка може виникнути в самому Promise або всередині будь-якого попереднього then.
Promise.reject(new Error("Не вдалося завантажити дані"))
.catch((error) => {
console.error("Помилка:", error.message);
});Помилку можна створити через throw у then:
Promise.resolve(5)
.then((value) => {
if (value < 10) {
throw new Error("Значення має бути не меншим за 10");
}
return value;
})
.then((value) => {
console.log("Результат:", value);
})
.catch((error) => {
console.error("Сталася помилка:", error.message);
});Після throw виконання пропускає наступні then і переходить до найближчого catch.
Promise.resolve("дані")
.then((value) => {
console.log("Крок 1:", value);
throw new Error("Помилка на кроці 1");
})
.then(() => {
console.log("Крок 2"); // Не виконається
})
.then(() => {
console.log("Крок 3"); // Не виконається
})
.catch((error) => {
console.error("Обробка помилки:", error.message);
});Виведення:
Крок 1: дані
Обробка помилки: Помилка на кроці 1catch може повернути звичайне значення. Тоді ланцюжок продовжиться як успішний.
Promise.reject(new Error("Сервер недоступний"))
.catch((error) => {
console.error(error.message);
return [];
})
.then((items) => {
console.log("Отримано елементів:", items.length);
});У цьому прикладі catch перетворює помилку на порожній масив. Наступний then отримує цей масив.
Якщо в catch знову кинути помилку або повернути відхилений Promise, ланцюжок залишиться невдалим:
Promise.reject(new Error("Помилка авторизації"))
.catch((error) => {
console.error("Початкова помилка:", error.message);
throw new Error("Не вдалося відновити сесію");
})
.then(() => {
console.log("Цей код не виконається");
})
.catch((error) => {
console.error("Фінальна помилка:", error.message);
});Це корисно, коли поточний рівень не може коректно обробити проблему і має передати її вище.
Помилку можна обробити безпосередньо після конкретного кроку або одним catch наприкінці ланцюжка.
catch наприкінціgetUser()
.then((user) => loadOrders(user.id))
.then((orders) => processOrders(orders))
.then((result) => console.log(result))
.catch((error) => {
console.error("Не вдалося виконати операцію:", error.message);
});Такий підхід зручний, коли для всіх помилок підходить одна стратегія: показати повідомлення, записати помилку в журнал або повернути значення за замовчуванням.
catchІноді окремий крок має власну логіку відновлення:
function loadSettings() {
return Promise.reject(new Error("Налаштування недоступні"));
}
function getDefaultSettings() {
return {
theme: "light",
language: "uk"
};
}
loadSettings()
.catch((error) => {
console.warn("Використовуємо налаштування за замовчуванням:", error.message);
return getDefaultSettings();
})
.then((settings) => {
console.log("Тема:", settings.theme);
});Помилка завантаження налаштувань не перериває весь процес, оскільки catch повертає запасне значення.
finallyfinally виконується незалежно від результату Promise:
після успішного завершення;
після помилки;
перед переходом до наступного обробника.
Його часто використовують для завершення спільних дій:
приховування індикатора завантаження;
розблокування кнопки;
закриття ресурсу;
очищення тимчасового стану.
function loadData() {
return new Promise((resolve) => {
setTimeout(() => {
resolve({ message: "Дані завантажено" });
}, 500);
});
}
console.log("Показуємо індикатор завантаження");
loadData()
.then((data) => {
console.log(data.message);
})
.catch((error) => {
console.error("Помилка:", error.message);
})
.finally(() => {
console.log("Приховуємо індикатор завантаження");
});finally не отримує результат Promise:
Promise.resolve(42)
.finally(() => {
console.log("Операція завершена");
})
.then((value) => {
console.log(value); // 42
});Значення 42 автоматично передається далі.
Так само finally не змінює помилку, якщо сам не кидає нову помилку:
Promise.reject(new Error("Помилка запиту"))
.finally(() => {
console.log("Очищення ресурсів");
})
.catch((error) => {
console.error(error.message); // Помилка запиту
});Якщо ж finally кидає власну помилку, вона замінить попередній результат:
Promise.resolve("успіх")
.finally(() => {
throw new Error("Помилка очищення");
})
.then((value) => {
console.log(value); // Не виконається
})
.catch((error) => {
console.error(error.message); // Помилка очищення
});Нижче наведено приклад послідовного завантаження користувача, перевірки його даних і завантаження замовлень.
function fetchUser() {
return new Promise((resolve, reject) => {
setTimeout(() => {
const user = {
id: 10,
name: "Iryna",
isActive: true
};
resolve(user);
}, 300);
});
}
function validateUser(user) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (!user.isActive) {
reject(new Error("Користувач неактивний"));
return;
}
resolve(user);
}, 300);
});
}
function fetchOrders(userId) {
return new Promise((resolve) => {
setTimeout(() => {
resolve([
{ id: 1, userId, total: 1200 },
{ id: 2, userId, total: 800 }
]);
}, 300);
});
}
console.log("Починаємо завантаження");
let isLoading = true;
fetchUser()
.then((user) => {
console.log(`Користувача знайдено: ${user.name}`);
return validateUser(user);
})
.then((user) => {
console.log("Користувача перевірено");
return fetchOrders(user.id);
})
.then((orders) => {
const total = orders.reduce((sum, order) => sum + order.total, 0);
console.log(`Кількість замовлень: ${orders.length}`);
console.log(`Загальна сума: ${total}`);
})
.catch((error) => {
console.error("Не вдалося завершити операцію:", error.message);
})
.finally(() => {
isLoading = false;
console.log("Завантаження завершено:", !isLoading);
});У цьому ланцюжку:
fetchUser повертає користувача.
Його результат передається у validateUser.
Після успішної перевірки ідентифікатор передається у fetchOrders.
catch обробляє помилку на будь-якому з попередніх кроків.
finally виконує завершальні дії в обох випадках.
returnfetchUser()
.then((user) => {
fetchOrders(user.id);
})
.then((orders) => {
console.log(orders); // undefined
});Потрібно повернути Promise:
fetchUser()
.then((user) => {
return fetchOrders(user.id);
})
.then((orders) => {
console.log(orders);
});Не варто вкладати then всередину іншого then, якщо можна повернути Promise:
fetchUser().then((user) => {
fetchOrders(user.id).then((orders) => {
console.log(orders);
});
});Такий код ускладнює централізовану обробку помилок. Краще побудувати плоский ланцюжок:
fetchUser()
.then((user) => fetchOrders(user.id))
.then((orders) => {
console.log(orders);
})
.catch((error) => {
console.error("Помилка:", error.message);
});catch обробляє не всі помилки програмиcatch обробляє лише помилки свого ланцюжка. Якщо асинхронний Promise не повернуто або не додано до ланцюжка, його помилка може залишитися необробленою.
Неправильно:
Promise.resolve()
.then(() => {
Promise.reject(new Error("Втрачена помилка"));
})
.catch((error) => {
console.error(error.message); // Цей код не виконається
});Правильно:
Promise.resolve()
.then(() => {
return Promise.reject(new Error("Помилка в ланцюжку"));
})
.catch((error) => {
console.error(error.message);
});catchЯкщо після catch виникне нова помилка, для неї потрібен наступний обробник або зовнішній catch:
Promise.reject(new Error("Перша помилка"))
.catch(() => {
throw new Error("Друга помилка");
})
.catch((error) => {
console.error(error.message); // Друга помилка
});finally для зміни результатуfinally призначений для завершальних дій, а не для перетворення даних. Для зміни значення використовуйте then, а для відновлення після помилки — catch.
Створіть ланцюжок, який:
отримує масив чисел через Promise;
залишає лише парні числа;
обчислює їхню суму;
кидає помилку, якщо масив порожній;
обробляє помилку через catch;
у finally виводить повідомлення про завершення обчислення.
Один із можливих варіантів:
function getNumbers() {
return Promise.resolve([3, 8, 11, 14, 20]);
}
getNumbers()
.then((numbers) => {
if (numbers.length === 0) {
throw new Error("Масив порожній");
}
return numbers.filter((number) => number % 2 === 0);
})
.then((evenNumbers) => {
return evenNumbers.reduce((sum, number) => sum + number, 0);
})
.then((sum) => {
console.log("Сума парних чисел:", sum);
})
.catch((error) => {
console.error("Помилка:", error.message);
})
.finally(() => {
console.log("Обчислення завершено");
});.then() створює наступний крок ланцюжка.
Значення між кроками передається через return.
Якщо повернути Promise, наступний крок дочекається його завершення.
throw у then перетворює результат ланцюжка на помилку.
.catch() обробляє помилки поточного ланцюжка.
catch може відновити виконання, повернувши звичайне значення.
finally() виконується незалежно від успіху чи помилки.
Для надійного ланцюжка не можна забувати return і залишати асинхронні операції поза ним.