Пошук уроків, статей та іншого контенту
Створите Promise, розберете його стани та навчитеся переводити асинхронний результат у зрозумілу модель.
Promise — це об’єкт, який представляє результат асинхронної операції, доступний зараз або згодом.
Наприклад, результат запиту до сервера не можна отримати миттєво:
const promise = loadData();На момент виклику loadData() даних може ще не бути. Promise дозволяє описати майбутній результат і визначити, що робити після його отримання:
loadData()
.then((data) => {
console.log("Дані отримано:", data);
})
.catch((error) => {
console.error("Помилка:", error);
});Promise не містить саму асинхронну операцію. Він містить інформацію про її майбутній результат:
результат ще не готовий;
результат успішно отримано;
під час виконання сталася помилка.
У Promise є три стани:
pending — очікування;
fulfilled — операція успішно завершена;
rejected — операція завершилася з помилкою.
Початковий стан кожного нового Promise — pending.
const promise = new Promise(() => {
// Promise залишається у стані pending
});
console.log(promise);Якщо операція успішна, Promise переходить у стан fulfilled і зберігає значення результату.
Якщо сталася помилка, Promise переходить у стан rejected і зберігає причину помилки.
Можливі лише такі переходи:
pending → fulfilled
pending → rejectedПісля переходу в fulfilled або rejected стан Promise вже не змінюється. Такий Promise називають завершеним або settled.
const promise = new Promise((resolve, reject) => {
resolve("Перше значення");
resolve("Друге значення");
reject(new Error("Помилка"));
});
promise.then((value) => {
console.log(value); // "Перше значення"
});Перший виклик resolve завершує Promise. Наступні виклики ігноруються.
Promise створюють за допомогою конструктора Promise:
const promise = new Promise((resolve, reject) => {
// асинхронна операція
});Конструктор отримує функцію-виконавець. Ця функція автоматично викликається одразу під час створення Promise.
Вона отримує два аргументи:
resolve — функція успішного завершення;
reject — функція завершення з помилкою.
const promise = new Promise((resolve, reject) => {
const isSuccessful = true;
if (isSuccessful) {
resolve("Операція успішна");
} else {
reject(new Error("Операція неуспішна"));
}
});Отримати результат Promise можна через метод then, а обробити помилку — через catch:
promise
.then((value) => {
console.log(value);
})
.catch((error) => {
console.error(error.message);
});Розглянемо функцію, яка імітує завантаження профілю користувача:
function loadUser() {
return new Promise((resolve, reject) => {
setTimeout(() => {
const user = {
id: 1,
name: "Олена",
};
resolve(user);
}, 1000);
});
}
console.log("Початок");
loadUser()
.then((user) => {
console.log("Користувач:", user);
})
.catch((error) => {
console.error("Помилка:", error.message);
});
console.log("Кінець");Результат виконання:
Початок
Кінець
Користувач: { id: 1, name: 'Олена' }loadUser() повертає Promise одразу, але користувач стає доступним приблизно через одну секунду.
Синхронний код не блокується: рядок console.log("Кінець") виконується до завершення таймера.
Функція може завершити Promise як успішно, так і з помилкою:
function loadUser(shouldSucceed) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (shouldSucceed) {
resolve({
id: 1,
name: "Олена",
});
} else {
reject(new Error("Не вдалося завантажити користувача"));
}
}, 500);
});
}
loadUser(false)
.then((user) => {
console.log("Користувач:", user);
})
.catch((error) => {
console.error(error.message);
});Якщо викликається resolve, виконується обробник then.
Якщо викликається reject, виконання переходить до найближчого обробника помилок catch.
resolve та причина rejectУ resolve можна передати будь-яке значення:
const numberPromise = new Promise((resolve) => {
resolve(42);
});
numberPromise.then((value) => {
console.log(value); // 42
});Це може бути об’єкт, масив, рядок або навіть інший Promise:
const userPromise = new Promise((resolve) => {
resolve({
id: 1,
name: "Олена",
});
});
userPromise.then((user) => {
console.log(user.name); // "Олена"
});У reject зазвичай передають об’єкт Error:
const promise = new Promise((resolve, reject) => {
reject(new Error("Сталася помилка"));
});
promise.catch((error) => {
console.error(error.message);
});Використання Error зберігає інформацію про помилку та робить її обробку передбачуваною.
thenМетод then приймає обробник успішного результату:
Promise.resolve(10).then((value) => {
console.log(value); // 10
});До then можна передати також обробник помилки:
Promise.reject(new Error("Помилка"))
.then(
(value) => {
console.log("Успіх:", value);
},
(error) => {
console.error("Помилка:", error.message);
},
);Проте для помилок зазвичай використовують окремий catch, оскільки такий код легше читати:
Promise.reject(new Error("Помилка"))
.then((value) => {
console.log("Успіх:", value);
})
.catch((error) => {
console.error("Помилка:", error.message);
});then завжди повертає новий Promise. Саме тому виклики можна об’єднувати в ланцюжок.
Promise.resolve(5)
.then((value) => {
return value * 2;
})
.then((value) => {
return value + 1;
})
.then((value) => {
console.log(value); // 11
});Значення, повернуте з одного обробника, передається в наступний then.
Це дає змогу послідовно обробляти результат:
function loadUser() {
return new Promise((resolve) => {
setTimeout(() => {
resolve({
id: 1,
name: "Олена",
});
}, 300);
});
}
function loadOrders(userId) {
return new Promise((resolve) => {
setTimeout(() => {
resolve([
{ id: 101, userId, total: 1200 },
{ id: 102, userId, total: 800 },
]);
}, 300);
});
}
loadUser()
.then((user) => {
console.log("Користувач:", user);
return loadOrders(user.id);
})
.then((orders) => {
console.log("Замовлення:", orders);
})
.catch((error) => {
console.error("Помилка:", error.message);
});Якщо з обробника повертається Promise, наступний then чекатиме на його завершення.
Якщо в обробнику виникла помилка або він повернув відхилений Promise, керування переходить до наступного catch.
Promise.resolve("Початок")
.then((value) => {
console.log(value);
throw new Error("Помилка під час обробки");
})
.then(() => {
console.log("Цей обробник не виконається");
})
.catch((error) => {
console.error(error.message);
});Помилки не потрібно обробляти в кожному then. Один catch наприкінці може обробити помилки всього ланцюжка.
Для вже відомого успішного результату використовують Promise.resolve:
const promise = Promise.resolve("Готово");
promise.then((value) => {
console.log(value);
});Для вже відомої помилки використовують Promise.reject:
const promise = Promise.reject(new Error("Щось пішло не так"));
promise.catch((error) => {
console.error(error.message);
});Ці методи корисні, коли функція має повертати Promise незалежно від того, чи результат доступний одразу.
function getConfiguration() {
const configuration = {
theme: "dark",
language: "uk",
};
return Promise.resolve(configuration);
}
getConfiguration().then((configuration) => {
console.log(configuration);
});Функція, яка виконує асинхронну операцію, зазвичай має повертати Promise, а не приймати callback для кожного результату.
function findProduct(productId) {
return new Promise((resolve, reject) => {
setTimeout(() => {
const products = [
{ id: 1, name: "Клавіатура" },
{ id: 2, name: "Миша" },
];
const product = products.find((item) => item.id === productId);
if (product) {
resolve(product);
} else {
reject(new Error("Товар не знайдено"));
}
}, 400);
});
}
findProduct(2)
.then((product) => {
console.log("Знайдено товар:", product.name);
})
.catch((error) => {
console.error(error.message);
});Така модель має чіткий контракт:
функція повертає Promise;
успіх передає значення через resolve;
помилка передає причину через reject;
викликаючий код сам вирішує, як обробити результат.
Функція не повинна сама вирішувати, що робити з результатом:
function calculateDiscount(price) {
return new Promise((resolve, reject) => {
if (price < 0) {
reject(new Error("Ціна не може бути від’ємною"));
return;
}
const discount = price * 0.1;
resolve(discount);
});
}
calculateDiscount(1000)
.then((discount) => {
console.log("Знижка:", discount);
})
.catch((error) => {
console.error("Не вдалося розрахувати знижку:", error.message);
});calculateDiscount лише створює результат або помилку. Код, який викликає функцію, відповідає за відображення повідомлення, запис у журнал або іншу реакцію.
Функція-виконавець запускається синхронно:
console.log("До створення");
const promise = new Promise((resolve) => {
console.log("Виконавець Promise");
resolve("Готово");
});
console.log("Після створення");
promise.then((value) => {
console.log(value);
});Результат:
До створення
Виконавець Promise
Після створення
ГотовоОбробник then виконується не під час виклику resolve, а пізніше — після завершення поточного синхронного коду.
Навіть якщо Promise вже завершений, обробник then виконається асинхронно:
const promise = Promise.resolve("Готово");
promise.then((value) => {
console.log(value);
});
console.log("Синхронний код");Результат:
Синхронний код
ГотовоЦя властивість забезпечує однакову модель роботи для вже доступних і майбутніх результатів.
Наведений приклад імітує перевірку статусу замовлення:
function checkOrderStatus(orderId) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (!Number.isInteger(orderId) || orderId <= 0) {
reject(new Error("Некоректний ідентифікатор замовлення"));
return;
}
const order = {
id: orderId,
status: "paid",
};
resolve(order);
}, 500);
});
}
checkOrderStatus(25)
.then((order) => {
if (order.status === "paid") {
console.log(`Замовлення №${order.id} оплачене`);
}
})
.catch((error) => {
console.error("Не вдалося перевірити замовлення:", error.message);
});Зверніть увагу на return після reject. Він не є обов’язковим для Promise, але запобігає подальшому виконанню коду всередині цього callback.
resolve або reject без умовиПомилково:
function getValue() {
return new Promise((resolve, reject) => {
resolve("Успіх");
reject(new Error("Помилка"));
});
}reject ніколи не спрацює, оскільки Promise вже завершено через resolve.
Правильно:
function getValue(isSuccessful) {
return new Promise((resolve, reject) => {
if (isSuccessful) {
resolve("Успіх");
return;
}
reject(new Error("Помилка"));
});
}return у ланцюжкуПомилково:
loadUser()
.then((user) => {
loadOrders(user.id);
})
.then((orders) => {
console.log(orders); // undefined
});Promise loadOrders не повертається в ланцюжок.
Правильно:
loadUser()
.then((user) => {
return loadOrders(user.id);
})
.then((orders) => {
console.log(orders);
});Якщо функція вже повертає Promise, не потрібно обгортати її в новий Promise:
function getData() {
return fetch("/api/data");
}Зайва обгортка ускладнює код і може призвести до помилок, якщо не викликати resolve або reject у всіх гілках.
Помилково:
loadUser().catch(() => {
// Помилка проігнорована
});Такий код приховує проблему. Якщо помилку потрібно лише зафіксувати, додайте зрозуміле повідомлення:
loadUser().catch((error) => {
console.error("Помилка завантаження користувача:", error);
});returnPromise не стає звичайним значенням лише через return:
function getUserName() {
return loadUser().then((user) => {
return user.name;
});
}getUserName() повертає Promise, тому результат потрібно обробити асинхронно:
getUserName().then((name) => {
console.log(name);
});Під час написання власних асинхронних функцій:
Повертайте Promise з функції.
Викликайте resolve лише для успішного результату.
Викликайте reject для помилок.
Передавайте в reject об’єкт Error.
Завершуйте виконання гілки після reject, якщо далі не потрібно виконувати код.
Повертайте вкладені Promise з обробників then.
Обробляйте помилки через catch.
Не створюйте зайву обгортку навколо вже наявного Promise.
Promise описує майбутній результат асинхронної операції.
Promise має стани pending, fulfilled і rejected.
Переходи між станами відбуваються лише з pending у fulfilled або rejected.
resolve завершує Promise успішно, а reject — з помилкою.
Після завершення стан Promise не змінюється.
then обробляє успішний результат і повертає новий Promise.
catch обробляє помилки поточного ланцюжка.
Значення між обробниками передаються через return.
Асинхронна функція зазвичай має повертати Promise з чітким контрактом результату або помилки.