Пошук уроків, статей та іншого контенту
Розберемо об’єкти Promise, їхні стани, ланцюжки then/catch/finally та перетворення результатів.
Promise — це об’єкт, який представляє результат асинхронної операції, доступний зараз або в майбутньому.
Наприклад, операція читання файлу, запит до сервера чи таймер можуть завершитися не одразу. Promise дозволяє описати, що потрібно зробити після успішного завершення операції або після помилки.
Promise має три стани:
pending — операція ще виконується;
fulfilled — операція успішно завершилася;
rejected — операція завершилася помилкою.
Після переходу в стан fulfilled або rejected Promise вже не змінює свій стан.
Для створення Promise використовують конструктор Promise:
const promise = new Promise((resolve, reject) => {
// Імітуємо асинхронну операцію
setTimeout(() => {
const operationSucceeded = true;
if (operationSucceeded) {
resolve("Операцію виконано успішно");
} else {
reject(new Error("Операція завершилася помилкою"));
}
}, 1000);
});
console.log("Операція запущена");
promise
.then((result) => {
console.log(result);
})
.catch((error) => {
console.error(error.message);
});Конструктор отримує функцію, яка виконується одразу. Вона має два параметри:
resolve(value) — повідомляє про успішне завершення;
reject(error) — повідомляє про помилку.
У цьому прикладі Promise спочатку перебуває у стані pending. Через секунду він переходить у стан fulfilled або rejected.
Значення, передане в resolve, можна отримати в обробнику then:
const numberPromise = new Promise((resolve) => {
resolve(42);
});
numberPromise.then((number) => {
console.log(number); // 42
});Причину помилки, передану в reject, можна отримати в catch:
const failedPromise = new Promise((resolve, reject) => {
reject(new Error("Не вдалося отримати дані"));
});
failedPromise.catch((error) => {
console.error(error.message); // Не вдалося отримати дані
});Зазвичай для помилки використовують об’єкт Error. Його властивість message містить текст повідомлення.
Метод then викликає функцію після успішного виконання Promise:
Promise.resolve("Дані отримано").then((result) => {
console.log(result);
});Promise.resolve(value) створює вже виконаний Promise.
Метод then також може отримувати другий аргумент для обробки помилки:
Promise.reject(new Error("Помилка запиту")).then(
(result) => {
console.log(result);
},
(error) => {
console.error(error.message);
}
);На практиці для помилок частіше використовують окремий метод catch, оскільки він краще читається.
Метод catch викликається, якщо Promise завершився помилкою:
Promise.reject(new Error("Не вдалося завантажити дані"))
.catch((error) => {
console.error("Помилка:", error.message);
});Помилка також може виникнути всередині then:
Promise.resolve("Початкове значення")
.then(() => {
throw new Error("Помилка під час обробки");
})
.catch((error) => {
console.error(error.message);
});catch обробляє як помилку початкового Promise, так і помилки, що виникли в попередніх обробниках ланцюжка.
Метод finally виконується незалежно від результату Promise:
Promise.resolve("Успіх")
.then((result) => {
console.log(result);
})
.finally(() => {
console.log("Операція завершена");
});Він також виконується у випадку помилки:
Promise.reject(new Error("Помилка"))
.catch((error) => {
console.error(error.message);
})
.finally(() => {
console.log("Прибирання ресурсів завершено");
});finally зручно використовувати для спільних дій:
приховування індикатора завантаження;
закриття ресурсу;
очищення тимчасових даних;
виведення повідомлення про завершення операції.
Функція finally не отримує результат або помилку. Її призначення — виконати завершальну дію.
Методи then, catch і finally повертають новий Promise. Завдяки цьому їх можна об’єднувати в ланцюжки:
Promise.resolve(5)
.then((number) => {
return number * 2;
})
.then((number) => {
return number + 10;
})
.then((result) => {
console.log(result); // 20
});Кожен наступний then отримує результат, повернений попереднім then.
Послідовність перетворень у цьому прикладі:
початкове значення — 5;
5 * 2 дає 10;
10 + 10 дає 20;
20 виводиться в консоль.
Щоб передати значення наступному обробнику, його потрібно повернути через return:
Promise.resolve("JavaScript")
.then((value) => {
return value.toUpperCase();
})
.then((value) => {
console.log(value); // JAVASCRIPT
});Якщо не повернути значення явно, наступний then отримає undefined:
Promise.resolve("JavaScript")
.then((value) => {
value.toUpperCase();
})
.then((value) => {
console.log(value); // undefined
});Обробник then може повернути інший Promise. Наступний then чекатиме на його завершення:
function getNumber() {
return new Promise((resolve) => {
setTimeout(() => {
resolve(10);
}, 500);
});
}
getNumber()
.then((number) => {
return new Promise((resolve) => {
setTimeout(() => {
resolve(number * 3);
}, 500);
});
})
.then((result) => {
console.log(result); // 30
});Так можна послідовно виконувати асинхронні операції.
Нижче наведено приклад, який можна запустити в Node.js:
function getUser() {
return new Promise((resolve) => {
setTimeout(() => {
resolve({
id: 1,
name: "Олена"
});
}, 300);
});
}
function getUserOrders(userId) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (userId !== 1) {
reject(new Error("Користувача не знайдено"));
return;
}
resolve([
{ id: 101, total: 250 },
{ id: 102, total: 400 }
]);
}, 300);
});
}
getUser()
.then((user) => {
console.log(`Користувач: ${user.name}`);
// Повертаємо Promise, щоб дочекатися замовлень
return getUserOrders(user.id);
})
.then((orders) => {
const total = orders.reduce((sum, order) => {
return sum + order.total;
}, 0);
return `Загальна сума замовлень: ${total}`;
})
.then((message) => {
console.log(message);
})
.catch((error) => {
console.error("Сталася помилка:", error.message);
})
.finally(() => {
console.log("Ланцюжок завершено");
});У цьому ланцюжку:
getUser повертає Promise з даними користувача;
перший then повертає Promise із замовленнями;
другий then обчислює загальну суму;
третій then виводить готове повідомлення;
catch обробляє помилки на будь-якому попередньому етапі;
finally виконується в кінці незалежно від результату.
Якщо обробник then повертає помилковий Promise або кидає помилку, виконання переходить до найближчого наступного catch:
Promise.resolve(10)
.then((number) => {
if (number < 20) {
throw new Error("Число замале");
}
return number;
})
.then((number) => {
console.log("Цей обробник не виконається:", number);
})
.catch((error) => {
console.error(error.message); // Число замале
});Після обробки помилки catch може повернути звичайне значення. Тоді наступний then отримає це значення:
Promise.reject(new Error("Не вдалося отримати дані"))
.catch((error) => {
console.error(error.message);
return "Запасне значення";
})
.then((value) => {
console.log(value); // Запасне значення
});Якщо ж у catch знову кинути помилку, Promise залишиться відхиленим:
Promise.reject(new Error("Початкова помилка"))
.catch((error) => {
console.error(error.message);
throw new Error("Не вдалося відновити операцію");
})
.catch((error) => {
console.error(error.message);
});Для створення Promise з готовим значенням використовують Promise.resolve:
const readyValue = Promise.resolve("Готове значення");
readyValue.then((value) => {
console.log(value);
});Для створення відхиленого Promise використовують Promise.reject:
const readyError = Promise.reject(new Error("Готова помилка"));
readyError.catch((error) => {
console.error(error.message);
});Ці методи корисні, коли функція має повертати Promise незалежно від того, чи є результат синхронним.
Якщо повернути Promise не повернути через return, наступний обробник не чекатиме його завершення:
function loadData() {
return new Promise((resolve) => {
setTimeout(() => resolve("Дані"), 500);
});
}
Promise.resolve()
.then(() => {
loadData();
})
.then((data) => {
console.log(data); // undefined
});Правильний варіант:
Promise.resolve()
.then(() => {
return loadData();
})
.then((data) => {
console.log(data); // Дані
});Якщо помилку передати другим аргументом then, це не завжди зручно для всього ланцюжка:
doSomething().then(
(result) => {
// Обробка результату
},
(error) => {
// Обробка помилки початкового Promise
}
);Для ланцюжків зазвичай зрозуміліше розмістити один catch у кінці:
doSomething()
.then((result) => {
return processResult(result);
})
.catch((error) => {
console.error(error.message);
});finally не отримує значення попереднього then:
Promise.resolve("Дані").finally((value) => {
console.log(value); // undefined
});Якщо потрібно використати результат, це слід робити в then, а finally залишити для завершальної дії.
Стан Promise змінюється лише один раз:
const promise = new Promise((resolve, reject) => {
resolve("Перше значення");
resolve("Друге значення");
reject(new Error("Помилка"));
});
promise.then((value) => {
console.log(value); // Перше значення
});Після першого resolve наступні виклики ігноруються.
Promise представляє майбутній результат асинхронної операції.
Promise має стани pending, fulfilled і rejected.
resolve завершує Promise успішно, а reject — з помилкою.
then обробляє успішний результат і повертає новий Promise.
Значення можна перетворювати, повертаючи його з then.
Якщо повернути інший Promise, ланцюжок чекатиме на його завершення.
catch обробляє помилки в Promise та попередніх обробниках.
finally виконує спільну завершальну дію незалежно від результату.
У ланцюжках важливо не забувати повертати Promise через return.