Promises
JavaScript Promises — офіціант у ресторані несе одночасно кілька тарілок обслуговуючи багато столиків
До цього моменту весь наш JavaScript був синхронним — кожен рядок виконувався по черзі. Але реальні застосунки потребують завантаження даних з сервера, читання файлів, очікування відповіді користувача. Все це займає час, і JavaScript має елегантний механізм для роботи з такими операціями — Promises.
Навіщо потрібна асинхронність?
Уяви: ти завантажуєш список користувачів з сервера. Запит може тривати 1-3 секунди. Якщо JavaScript буде чекати відповіді — вся сторінка "заморозиться": кнопки не працюватимуть, анімації зупиняться.
// Проблема: синхронний код блокує все
const data = fetchFromServer("https://api.example.com/users"); // ⏳ 2 секунди чекання
console.log(data); // Все заморожено, поки чекаємо!
JavaScript вирішує це асинхронністю — запит відправляється, а код продовжує виконуватися. Коли відповідь приходить, викликається callback.
Event Loop — як працює асинхронність
JavaScript — однопоточна мова. Він має лише один потік виконання. Але завдяки event loop може обробляти асинхронні операції.
Event Loop: Call Stack, Web APIs, Task Queue
| Компонент | Що робить |
|---|---|
| Call Stack | Виконує функції по черзі (LIFO) |
| Web APIs | Обробляють таймери, мережеві запити, DOM події |
| Task Queue | Черга callback-ів, готових до виконання |
| Event Loop | Переміщує задачі з черги в call stack, коли він порожній |
console.log("1: Початок");
setTimeout(() => {
console.log("2: Таймер спрацював");
}, 0); // навіть з 0 мс — це асинхронна операція!
console.log("3: Кінець");
// Результат:
// "1: Початок"
// "3: Кінець"
// "2: Таймер спрацював"
setTimeout(() => {}, 0) не виконується миттєво! Callback потрапляє в Task Queue і чекає, поки call stack звільниться. Тому "3: Кінець" завжди виведеться раніше за "2: Таймер спрацював".
Callbacks — старий підхід
До Promises для асинхронного коду використовували callbacks — функції, які передавались як аргументи:
function loadUser(id, onSuccess, onError) {
setTimeout(() => {
if (id > 0) {
onSuccess({ id, name: "Олексій" });
} else {
onError("Невірний ID");
}
}, 1000);
}
loadUser(
1,
(user) => console.log("Користувач:", user.name),
(error) => console.log("Помилка:", error)
);
Проблема виникає, коли потрібно виконати кілька операцій послідовно — Callback Hell:
loadUser(1, (user) => {
loadPosts(user.id, (posts) => {
loadComments(posts[0].id, (comments) => {
loadReplies(comments[0].id, (replies) => {
console.log(replies); // 😵 "Пірамідка смерті"
}, handleError);
}, handleError);
}, handleError);
}, handleError);
Callback Hell (або "пірамідка смерті") робить код нечитабельним та складним для підтримки. Promises вирішують цю проблему через chaining.
Що таке Promise?
Promise (обіцянка) — це об'єкт, який представляє результат асинхронної операції. Він може бути в одному з трьох станів:
| Стан | Опис |
|---|---|
| pending | Початковий стан, операція ще виконується |
| fulfilled | Операція успішно завершилась (є результат) |
| rejected | Операція завершилась з помилкою |
Після переходу у fulfilled або rejected стан більше не змінюється.
Створення Promise
const myPromise = new Promise((resolve, reject) => {
// Асинхронна операція
setTimeout(() => {
const success = true;
if (success) {
resolve("Дані отримано!"); // fulfilled
} else {
reject("Щось пішло не так"); // rejected
}
}, 1000);
});
resolve(value)— викликається при успіху, передає результатreject(error)— викликається при помилці
Використання: then, catch, finally
.then() — обробка успіху
myPromise.then((result) => {
console.log(result); // "Дані отримано!"
});
.catch() — обробка помилки
myPromise.catch((error) => {
console.log("Помилка:", error);
});
.finally() — виконається завжди
myPromise.finally(() => {
console.log("Операція завершена (успішно чи ні)");
});
Повний приклад
function fetchUser(id) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (id > 0) {
resolve({ id, name: "Олексій", age: 25 });
} else {
reject(new Error("ID має бути додатнім числом"));
}
}, 1000);
});
}
fetchUser(1)
.then((user) => console.log("Користувач:", user.name))
.catch((error) => console.log("Помилка:", error.message))
.finally(() => console.log("Запит завершено"));
// Через 1 секунду:
// "Користувач: Олексій"
// "Запит завершено"
Chaining — ланцюжки промісів
Головна перевага Promises — можливість chaining (ланцюжків). Кожен .then() повертає новий Promise:
fetchUser(1)
.then((user) => {
console.log("Користувач:", user.name);
return user.id; // Передаємо ID далі
})
.then((userId) => {
console.log("Завантажуємо пости для ID:", userId);
return fetchPosts(userId); // Повертаємо Promise
})
.then((posts) => {
console.log("Отримано постів:", posts.length);
})
.catch((error) => {
// Ловить помилку з БУДЬ-ЯКОГО .then() вище
console.log("Помилка:", error.message);
});
Один .catch() в кінці ланцюжка ловить помилки з усіх попередніх .then(). Не потрібно обробляти помилки на кожному кроці.
Promise.all() — паралельне виконання
Коли потрібно виконати кілька незалежних промісів одночасно:
const promise1 = fetchUser(1); // 1 сек
const promise2 = fetchUser(2); // 1 сек
const promise3 = fetchUser(3); // 1 сек
// Всі три запити виконуються паралельно!
Promise.all([promise1, promise2, promise3])
.then((users) => {
// users — масив результатів [user1, user2, user3]
console.log("Всі користувачі:", users);
})
.catch((error) => {
// Якщо ХОЧА Б ОДИН промис rejected — весь Promise.all rejected
console.log("Помилка:", error.message);
});
// Загальний час: ~1 сек (не 3!)
Promise.all() "падає" при першій помилці. Якщо потрібно отримати результати всіх промісів (навіть з помилками), використовуй Promise.allSettled().
Promise.race() — перший результат
Повертає результат промісу, який завершився першим:
const fast = new Promise((resolve) => setTimeout(() => resolve("Швидкий"), 100));
const slow = new Promise((resolve) => setTimeout(() => resolve("Повільний"), 2000));
Promise.race([fast, slow]).then((result) => {
console.log(result); // "Швидкий"
});
Корисно для тайм-аутів:
function fetchWithTimeout(url, ms) {
const fetchPromise = fetch(url);
const timeout = new Promise((_, reject) =>
setTimeout(() => reject(new Error("Тайм-аут!")), ms)
);
return Promise.race([fetchPromise, timeout]);
}
Promise.allSettled()
На відміну від Promise.all(), чекає завершення всіх промісів:
const promises = [
fetchUser(1), // успіх
fetchUser(-1), // помилка
fetchUser(3), // успіх
];
Promise.allSettled(promises).then((results) => {
results.forEach((result) => {
if (result.status === "fulfilled") {
console.log("Успіх:", result.value);
} else {
console.log("Помилка:", result.reason.message);
}
});
});
Типові помилки
1. Забули повернути значення в .then()
// ❌ Неправильно — result буде undefined
fetchUser(1)
.then((user) => {
fetchPosts(user.id); // забули return!
})
.then((posts) => {
console.log(posts); // undefined 😱
});
// ✅ Правильно
fetchUser(1)
.then((user) => {
return fetchPosts(user.id);
})
.then((posts) => {
console.log(posts); // масив постів
});
2. Забули .catch()
// ❌ Помилка "проковтнеться" без сліду
fetchUser(-1).then((user) => console.log(user));
// ✅ Завжди додавай .catch()
fetchUser(-1)
.then((user) => console.log(user))
.catch((error) => console.error("Помилка:", error.message));
Практика
Створи функцію, яка симулює завантаження даних:
function loadData(name, delay, shouldFail = false) {
return new Promise((resolve, reject) => {
console.log(`Завантаження ${name}...`);
setTimeout(() => {
if (shouldFail) {
reject(new Error(`Не вдалося завантажити ${name}`));
} else {
resolve(`${name}: дані отримано`);
}
}, delay);
});
}
// Послідовне виконання
loadData("Профіль", 1000)
.then((result) => {
console.log(result);
return loadData("Пости", 800);
})
.then((result) => {
console.log(result);
return loadData("Коментарі", 600);
})
.then((result) => console.log(result))
.catch((error) => console.error(error.message));
// Паралельне виконання
Promise.all([
loadData("Профіль", 1000),
loadData("Пости", 800),
loadData("Коментарі", 600),
])
.then((results) => console.log("Все завантажено:", results))
.catch((error) => console.error(error.message));
Підсумок
- Асинхронність дозволяє не блокувати виконання коду під час очікування
- Event Loop переміщує готові callback-и з черги в call stack
- Promise — об'єкт з трьома станами: pending, fulfilled, rejected
.then()— обробка успіху,.catch()— обробка помилки,.finally()— завжди- Chaining замінює callback hell плоским ланцюжком
Promise.all()— паралельне виконання (падає при першій помилці)Promise.race()— результат найшвидшого промісуPromise.allSettled()— чекає всіх, повертає статус кожного
Що далі?
У наступному уроці — async/await, який робить асинхронний код ще простішим та читабельнішим.
Корисні посилання:
- javascript.info: Promises — повний підручник (українською)
- MDN: Promise — документація
- JavaScript Visualizer — візуалізація event loop
- Loupe — інтерактивна візуалізація Event Loop від Philip Roberts
Відео:
- What the heck is the event loop anyway? — Philip Roberts — легендарна доповідь (27 хв), яка змінила розуміння JavaScript для мільйонів розробників