Співбесіда на посаду JavaScript-розробника майже неминуче торкнеться теми асинхронного програмування. І центральне місце в цій темі займають ES6+ Promises (Проміси). Глибоке розуміння того, як вони працюють, чому вони існують і як їх ефективно використовувати, є критично важливим для успішного проходження технічного інтерв’ю.
Ця стаття – ваш вичерпний посібник для підготовки. Ми розберемо все: від основних концепцій до складних сценаріїв використання та типових питань на співбесіді. Мета – не просто запам’ятати синтаксис, а зрозуміти механізми та принципи, що стоять за Promises.
Що таке Promise в JavaScript? Чому вони важливі?
У своїй основі Promise – це об’єкт, який представляє кінцевий результат (успіх або невдачу) асинхронної операції. Подумайте про це як про обіцянку: ви робите запит (наприклад, до сервера), і вам обіцяють надати результат у майбутньому. Цей результат може бути або даними, які ви очікували (успіх), або повідомленням про помилку (невдача).
Чому вони з’явилися і чому важливі?
До появи Promises, асинхронні операції в JavaScript переважно оброблялися за допомогою функцій зворотного виклику (callbacks). Це часто призводило до стану, відомого як “Callback Hell” або “Pyramid of Doom” – глибоко вкладеної структури колбеків, яку важко читати, розуміти та підтримувати.
JavaScript
// Приклад "Callback Hell"
asyncOperation1(data1, (error1, result1) => {
if (error1) {
handleError(error1);
} else {
asyncOperation2(result1, (error2, result2) => {
if (error2) {
handleError(error2);
} else {
asyncOperation3(result2, (error3, result3) => {
if (error3) {
handleError(error3);
} else {
// ... і так далі
console.log('Успіх!', result3);
}
});
}
});
}
});
Promises були введені в стандарті ES6 (ECMAScript 2015) для вирішення цих проблем. Вони пропонують більш структурований та читабельний спосіб роботи з асинхронним кодом, покращуючи обробку помилок та композицію операцій.
Ключові переваги Promises:
- Читабельність: Дозволяють писати асинхронний код, який виглядає більш послідовним та лінійним.
- Обробка помилок: Надають централізований механізм (
.catch()
) для відловлювання помилок у ланцюжку асинхронних операцій. - Композиція: Легко об’єднувати кілька асинхронних операцій (наприклад, за допомогою
Promise.all()
або.then()
chaining). - Стандартизація: Є стандартним способом представлення асинхронних операцій у сучасному JavaScript та багатьох API (Fetch API, бази даних тощо).
Життєвий цикл Promise: Стани
Кожен Promise знаходиться в одному з трьох станів:
- Pending (Очікування): Початковий стан. Операція ще не завершена. Promise “очікує” на результат.
- Fulfilled (Виконано успішно): Операція завершилася успішно. Promise “виконався” і має кінцеве значення (result).
- Rejected (Відхилено): Операція завершилася з помилкою. Promise “відхилено” і має причину відхилення (error/reason).
Стани Fulfilled
та Rejected
є кінцевими. Як тільки Promise переходить в один із цих станів, він більше ніколи не змінить свій стан або значення/причину. Кажуть, що Promise “settled” (врегульований), коли він вже не pending
.
Створення та використання Promises
Синтаксис створення Promise
Promise створюється за допомогою конструктора new Promise()
. Він приймає один аргумент – функцію-виконавця (executor
), яка, в свою чергу, отримує два аргументи: resolve
та reject
.
resolve(value)
: Функція, яку потрібно викликати, коли асинхронна операція успішно завершилася.value
– це результат операції. Це переводить Promise у станFulfilled
.reject(reason)
: Функція, яку потрібно викликати, коли сталася помилка.reason
– це об’єкт помилки або інша інформація про причину невдачі. Це переводить Promise у станRejected
.
<!– end list –>
JavaScript
const myPromise = new Promise((resolve, reject) => {
// Симуляція асинхронної операції (наприклад, запит до API)
setTimeout(() => {
const success = Math.random() > 0.3; // 70% шанс на успіх
if (success) {
const data = { message: "Дані успішно отримано!" };
resolve(data); // Операція успішна -> Fulfilled
} else {
const error = new Error("Не вдалося отримати дані.");
reject(error); // Операція неуспішна -> Rejected
}
}, 1000); // Затримка 1 секунда
});
console.log(myPromise); // На початку виведе: Promise { <pending> }
H3: Обробка результатів: .then()
Метод .then()
використовується для реєстрації функцій зворотного виклику, які будуть виконані, коли Promise перейде в стан Fulfilled
або Rejected
.
Він приймає до двох аргументів:
onFulfilled
: Функція, що викликається при успішному виконанні (станFulfilled
). Отримує результат (value), переданий уresolve
.onRejected
(опціонально): Функція, що викликається при помилці (станRejected
). Отримує причину (reason), передану вreject
.
<!– end list –>
JavaScript
myPromise.then(
(result) => {
// Ця функція виконається, якщо myPromise став Fulfilled
console.log("Успіх:", result.message);
},
(error) => {
// Ця функція виконається, якщо myPromise став Rejected
console.error("Помилка:", error.message);
}
);
Важливо: Метод .then()
завжди повертає новий Promise. Це дозволяє створювати ланцюжки (chaining), про які ми поговоримо далі.
H3: Обробка помилок: .catch()
Хоча помилки можна обробляти у другому аргументі .then()
, більш поширеним і зручним є використання методу .catch()
. Він приймає один аргумент:
onRejected
: Функція, що викликається, якщо Promise (або будь-який попередній Promise у ланцюжку) був відхилений (Rejected
).
<!– end list –>
JavaScript
myPromise
.then((result) => {
console.log("Успіх:", result.message);
// Можемо повернути значення для наступного .then()
return result.message.toUpperCase();
})
.then((upperCaseMessage) => {
console.log("Повідомлення у верхньому регістрі:", upperCaseMessage);
})
.catch((error) => {
// Цей блок обробить помилку *з будь-якого* попереднього етапу
console.error("Сталася помилка в ланцюжку:", error.message);
});
.catch(onRejected)
є синтаксичним цукром для .then(null, onRejected)
. Використання .catch()
покращує читабельність і дозволяє централізовано обробляти помилки в кінці ланцюжка.
Завжди виконується: .finally()
Метод .finally()
дозволяє зареєструвати функцію, яка буде виконана незалежно від того, чи був Promise успішно виконаний (Fulfilled
) чи відхилений (Rejected
). Він не отримує жодних аргументів (ні результату, ні помилки).
Це корисно для виконання дій “очищення”, таких як закриття з’єднань, приховування індикаторів завантаження тощо, які потрібно зробити в будь-якому випадку.
JavaScript
showLoadingSpinner(); // Показати індикатор завантаження
fetchDataPromise()
.then(data => processData(data))
.catch(error => console.error("Помилка завантаження:", error))
.finally(() => {
hideLoadingSpinner(); // Сховати індикатор завантаження (завжди)
console.log("Операція завершена (успішно чи ні).");
});
Ланцюжки Promises (Promise Chaining)
Одна з найпотужніших можливостей Promises – це здатність створювати ланцюжки асинхронних операцій. Оскільки .then()
(та .catch()
, .finally()
) повертає новий Promise, ми можемо послідовно викликати ці методи.
- Якщо
onFulfilled
абоonRejected
всередині.then()
повертає значення, наступний.then()
у ланцюжку отримає це значення як результат. - Якщо
onFulfilled
абоonRejected
всередині.then()
повертає новий Promise, наступний.then()
чекатиме, доки цей новий Promise не буде врегульований, і отримає його результат/причину. - Якщо
onFulfilled
абоonRejected
кидає помилку (throw new Error()
), наступний.catch()
у ланцюжку буде викликаний з цією помилкою.
<!– end list –>
JavaScript
// Приклад ланцюжка
fetchUserData(userId) // Повертає Promise<User>
.then(user => {
console.log("Отримано користувача:", user.name);
// Наступна асинхронна операція, залежна від попередньої
return fetchUserPosts(user.id); // Повертає Promise<Posts[]>
})
.then(posts => {
console.log(`Отримано ${posts.length} постів`);
// Можемо повернути просте значення
return posts.length;
})
.then(postCount => {
console.log(`Загальна кількість постів: ${postCount}`);
})
.catch(error => {
// Обробка помилок з fetchUserData, fetchUserPosts або будь-яких .then()
console.error("Не вдалося отримати дані користувача або пости:", error);
})
.finally(() => {
console.log("Завершено спробу отримання даних.");
});
Ланцюжки роблять асинхронний код набагато чистішим і зрозумілішим порівняно з “Callback Hell”.
Статичні методи Promise
Клас Promise
надає кілька корисних статичних методів для роботи з групами Promises:
Promise.all(iterable)
Приймає ітерабельний об’єкт (наприклад, масив) Promises. Повертає новий Promise, який:
- Виконується (
Fulfilled
), коли всі Promises у переданому масиві виконані успішно. Результатом буде масив значень цих Promises у тому ж порядку. - Відхиляється (
Rejected
), як тільки перший Promise у масиві відхиляється. Причиною буде причина відхилення цього першого Promise.
Використання: Коли потрібно виконати кілька незалежних асинхронних операцій паралельно і дочекатися їх усіх.
JavaScript
const promise1 = Promise.resolve(3);
const promise2 = 42; // Не-Promise значення трактуються як вже виконані Promises
const promise3 = new Promise((resolve) => setTimeout(resolve, 100, 'foo'));
Promise.all([promise1, promise2, promise3])
.then((values) => {
console.log(values); // Виведе: [3, 42, "foo"]
})
.catch(error => {
// Цей блок виконається, якщо хоча б один Promise відхилиться
console.error("Один з промісів в all() відхилився:", error);
});
Promise.race(iterable)
Приймає ітерабельний об’єкт Promises. Повертає новий Promise, який:
- Виконується або відхиляється, як тільки перший Promise у переданому масиві виконується або відхиляється. Результат/причина буде такою ж, як у цього першого врегульованого Promise.
Використання: Коли потрібно отримати результат від найшвидшої з кількох асинхронних операцій (наприклад, запит до кількох дзеркал сервера) або для реалізації таймаутів.
JavaScript
const promiseFast = new Promise((resolve) => setTimeout(resolve, 100, 'швидкий'));
const promiseSlow = new Promise((resolve) => setTimeout(resolve, 500, 'повільний'));
Promise.race([promiseFast, promiseSlow])
.then((value) => {
console.log(value); // Виведе: "швидкий"
});
// Приклад з таймаутом
const dataPromise = fetchData(); // Якийсь довгий запит
const timeoutPromise = new Promise((_, reject) =>
setTimeout(() => reject(new Error('Таймаут запиту')), 5000) // 5 секунд
);
Promise.race([dataPromise, timeoutPromise])
.then(data => console.log("Дані отримано:", data))
.catch(error => console.error("Помилка або таймаут:", error.message));
Promise.allSettled(iterable)
(ES2020)
Приймає ітерабельний об’єкт Promises. Повертає новий Promise, який:
- Завжди виконується (
Fulfilled
), коли всі Promises у переданому масиві врегульовані (тобтоFulfilled
абоRejected
). - Результатом буде масив об’єктів, що описують стан кожного Promise:
{ status: 'fulfilled', value: ... }
для успішних.{ status: 'rejected', reason: ... }
для відхилених.
Використання: Коли потрібно дочекатися завершення всіх операцій, незалежно від їхнього успіху чи невдачі, і отримати інформацію про кожну. На відміну від Promise.all
, він не “падає” при першій помилці.
JavaScript
const p1 = Promise.resolve('Успіх 1');
const p2 = Promise.reject(new Error('Помилка 2'));
const p3 = Promise.resolve('Успіх 3');
Promise.allSettled([p1, p2, p3])
.then((results) => {
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
console.log(`Promise ${index + 1} виконано: ${result.value}`);
} else {
console.error(`Promise ${index + 1} відхилено: ${result.reason.message}`);
}
});
});
/*
Виведе:
Promise 1 виконано: Успіх 1
Promise 2 відхилено: Помилка 2
Promise 3 виконано: Успіх 3
*/
Promise.any(iterable)
(ES2021)
Приймає ітерабельний об’єкт Promises. Повертає новий Promise, який:
- Виконується (
Fulfilled
), як тільки перший Promise у масиві виконається успішно (Fulfilled
). Результатом буде значення цього першого успішного Promise. - Відхиляється (
Rejected
), тільки якщо всі Promises у масиві відхилені. Причиною буде спеціальний об’єктAggregateError
, який містить масив причин відхилення всіх Promises.
Використання: Коли є кілька джерел для отримання даних, і вам потрібен результат від першого успішного.
JavaScript
const pErr1 = Promise.reject("Помилка 1");
const pSlow = new Promise((resolve) => setTimeout(resolve, 500, 'Повільний успіх'));
const pFast = new Promise((resolve) => setTimeout(resolve, 100, 'Швидкий успіх'));
Promise.any([pErr1, pSlow, pFast])
.then((value) => {
console.log(value); // Виведе: "Швидкий успіх"
})
.catch(error => {
// Цей блок виконається, тільки якщо всі проміси відхиляться
console.error("Всі проміси відхилені:", error);
});
Promise.resolve(value)
та Promise.reject(reason)
Promise.resolve(value)
: Створює Promise, який вже знаходиться у станіFulfilled
із зазначенимvalue
. Якщоvalue
саме є Promise, то повертається цей самий Promise.Promise.reject(reason)
: Створює Promise, який вже знаходиться у станіRejected
із зазначеноюreason
.
Використання: Для створення “обгорток” над значеннями, які можуть бути або не бути Promises, або для швидкого створення вже врегульованих Promises (наприклад, у тестах або для негайного початку ланцюжка).
Async/Await: Синтаксичний цукор над Promises
ES2017 (ES8) представив синтаксис async/await
, який побудований поверх Promises і дозволяє писати асинхронний код так, ніби він синхронний. Це значно покращує читабельність.
async
: Ключове слово перед оголошенням функції (async function myFunc() { ... }
). Воно робить дві речі:- Змушує функцію завжди повертати Promise. Якщо функція повертає значення, воно автоматично загортається в
Promise.resolve()
. Якщо функція кидає помилку, повертаєтьсяPromise.reject()
. - Дозволяє використовувати ключове слово
await
всередині цієї функції.
- Змушує функцію завжди повертати Promise. Якщо функція повертає значення, воно автоматично загортається в
await
: Ключове слово, яке можна використовувати тільки всерединіasync
функції. Воно ставиться перед викликом функції, що повертає Promise.await
“паузить” виконанняasync
функції доти, доки Promise не буде врегульований (Fulfilled
абоRejected
).- Якщо Promise виконується (
Fulfilled
),await
повертає його результат. - Якщо Promise відхиляється (
Rejected
),await
кидає помилку (причину відхилення).
- Якщо Promise виконується (
<!– end list –>
JavaScript
// Функція, що повертає Promise
function delay(ms, value) {
return new Promise(resolve => setTimeout(() => resolve(value), ms));
}
// Використання async/await
async function runAsyncTask() {
console.log("Початок...");
try {
const result1 = await delay(1000, "Крок 1 завершено"); // Пауза 1 сек
console.log(result1);
const result2 = await delay(500, "Крок 2 завершено"); // Пауза 0.5 сек
console.log(result2);
// Симуляція помилки
// await Promise.reject(new Error("Щось пішло не так!"));
console.log("...Кінець");
return "Все пройшло успішно!"; // Це значення буде результатом Promise, що повертає runAsyncTask
} catch (error) {
console.error("Спіймано помилку:", error.message);
// Якщо потрібно, можна повернути значення помилки або кинути далі
throw error; // Повторно кинути помилку, щоб Promise функції відхилився
}
}
// Виклик async функції
runAsyncTask()
.then(finalResult => console.log("Результат async функції:", finalResult))
.catch(error => console.error("Остаточна помилка async функції:", error.message));
Переваги async/await
:
- Читабельність: Код виглядає майже як синхронний.
- Обробка помилок: Використовується стандартний
try...catch
блок, що є звичним для багатьох розробників. - Дебаггінг: Легше відстежувати потік виконання.
Важливо пам’ятати: async/await
– це лише синтаксичний цукор. Під капотом все ще працюють Promises.
Типові питання на співбесіді про Promises
Інтерв’юер може поставити різні питання, щоб перевірити ваше розуміння Promises:
- Що таке Promise? Яку проблему він вирішує?
- Відповідь: Поясніть концепцію (об’єкт для майбутнього результату), проблему “Callback Hell” і як Promises покращують читабельність, обробку помилок та композицію асинхронного коду.
- Назвіть стани Promise.
- Відповідь:
pending
,fulfilled
,rejected
. Поясніть, що означає кожен стан і як відбувається перехід між ними (черезresolve
/reject
). Згадайте, щоfulfilled
таrejected
є кінцевими (settled).
- Відповідь:
- Як створити Promise?
- Відповідь: Покажіть синтаксис
new Promise((resolve, reject) => { ... })
. Поясніть рольexecutor
,resolve
таreject
.
- Відповідь: Покажіть синтаксис
- Що роблять
.then()
,.catch()
,.finally()
?- Відповідь:
.then()
– для обробки успішного результату (та опціонально помилки), повертає новий Promise..catch()
– для централізованої обробки помилок, повертає новий Promise..finally()
– для коду, що виконується завжди (очищення), повертає новий Promise.
- Відповідь:
- Що таке Promise Chaining? Як воно працює?
- Відповідь: Поясніть, що
.then
/.catch
/.finally
повертають Promises, дозволяючи викликати їх послідовно. Розкажіть, як передаються значення або Promises між етапами ланцюжка.
- Відповідь: Поясніть, що
- Поясніть
Promise.all()
,Promise.race()
,Promise.allSettled()
,Promise.any()
. У чому різниця та коли використовувати кожен?- Відповідь: Чітко опишіть поведінку кожного методу (коли він виконується/відхиляється, який результат повертає). Наведіть приклади використання для кожного (паралельні запити, найшвидший результат, отримання всіх результатів незалежно від помилок, перший успішний).
- Що таке
async/await
? Як воно пов’язане з Promises?- Відповідь: Поясніть як синтаксичний цукор над Promises. Розкажіть про
async
функції (завжди повертають Promise) таawait
(паузить виконання до врегулювання Promise). Покажіть переваги (читабельність,try...catch
).
- Відповідь: Поясніть як синтаксичний цукор над Promises. Розкажіть про
- Як обробляти помилки при використанні Promises? А при
async/await
?- Відповідь: З Promises – за допомогою
.catch()
або другого аргументу.then()
. Зasync/await
– за допомогою стандартного блокуtry...catch
.
- Відповідь: З Promises – за допомогою
- Напишіть код, який робить X за допомогою Promises (наприклад, отримує дані з двох API паралельно і об’єднує результати).
- Підготовка: Практикуйтеся писати код з різними сценаріями використання Promises та їх статичних методів.
Поради для успішної співбесіди
- Практика, практика, практика: Не просто читайте, а пишіть код. Створюйте власні Promises, використовуйте
Workspace
API, симулюйте затримки зsetTimeout
. - Пояснюйте вголос: Коли пишете код на співбесіді (або під час підготовки), коментуйте свої дії та пояснюйте, чому ви робите саме так.
- Знайте “Чому”: Розумійте не тільки як щось працює, але й чому воно було створено і які проблеми вирішує.
- Порівнюйте: Будьте готові порівняти Promises з колбеками та
async/await
, пояснити переваги та недоліки кожного підходу. - Реальні сценарії: Подумайте, де ви використовували Promises у своїх проектах (API запити, анімації, робота з файлами/базами даних в Node.js). Це покаже ваш практичний досвід.
- Не бійтеся сказати “Не знаю”: Якщо ви не впевнені, краще сказати “Я не впевнений на 100%, але я думаю, що…” або “Я не стикався з
Promise.any()
, але судячи з назви та аналогії зPromise.all()
, я припускаю, що…”, ніж намагатися вгадати неправильно.
Висновок
Розуміння ES6+ Promises є абсолютно необхідним для сучасного JavaScript-розробника. Це не просто синтаксис, а фундаментальна концепція для керування асинхронністю. На співбесіді від вас очікуватимуть не тільки знання API (.then
, .catch
, Promise.all
тощо), але й глибокого розуміння їхньої поведінки, станів, механізмів обробки помилок та зв’язку з async/await
.
Приділіть час на вивчення та практику – і ви будете впевнено почуватися на співбесіді, відповідаючи на питання про Promises. Успіхів!
Додаткові ресурси для поглибленого вивчення:
MDN Web Docs: Promises JavaScript.info: Promises, async/await
Стаття оновлена станом на 30 квітня 2025 року.