Skip to main content

Проміси в JavaScript та методи promise

Promise - це об'єкт JavaScript, що представляє майбутній результат асинхронної операції: успіх або помилку.

Теорія

TL;DR

  • Аналогія: замовлення їжі з доставкою. Отримуєш талон (Promise) одразу. Потім або їжа приїжджає (fulfilled), або приходить повідомлення «немає в наявності» (rejected). Одна відповідь, один раз.
  • Головна відмінність від callback: Promise є одним об'єктом з ланцюжком .then(). Callback-функції вкладаються одна в одну, утворюючи глибоко вкладений код.
  • Стани фінальні: після того як Promise завершився (fulfilled або rejected), він більше не змінюється.
  • Правила вибору методу: Promise.all() коли все має успішно завершитись, Promise.allSettled() для часткового успіху, Promise.race() для найшвидшого результату, Promise.any() для першого успішного.

Швидкий приклад

javascript
// Імітує API-запит const fetchData = new Promise((resolve, reject) => { setTimeout(() => { const ok = Math.random() > 0.5; ok ? resolve("Дані завантажено") : reject("Мережева помилка"); }, 1000); }); // Споживаємо Promise fetchData .then(result => console.log(result)) // "Дані завантажено" .catch(error => console.error(error)); // "Мережева помилка"

Через 1 секунду спрацьовує рівно одна гілка. Promise завершується одного разу і залишається в цьому стані.

Стани Promise

Promise завжди перебуває в одному з трьох станів:

  • Pending: асинхронна операція ще виконується, результату немає
  • Fulfilled: викликано resolve(value), операція успішна
  • Rejected: викликано reject(error), операція завершилась помилкою

Pending є початковим станом. Fulfilled і rejected є фінальними. На відміну від браузерних подій, Promise спрацьовує рівно один раз і залишається завершеним.

Головна відмінність від callback

Callback-функції вкладаються. Кожен новий асинхронний крок додає ще один рівень відступу, і обробка помилок потрібна на кожному рівні окремо. Promise ланцюгується: кожен .then() повертає новий Promise, тому рівень відступу залишається одним. Помилки автоматично «падають» до найближчого .catch(). Саме це усуває callback hell для будь-якого коду глибше одного асинхронного кроку.

Методи Promise

Promise.all()

Запускає всі проміси паралельно і чекає поки кожен з них завершиться успішно. Повертає масив результатів у тому ж порядку, що й вхідний масив. Якщо хоча б один проміс відхиляється, весь результат одразу відхиляється з цією помилкою. Решта промісів продовжують виконуватись, але їхні результати ігноруються.

javascript
const [user, posts] = await Promise.all([ fetch('/api/user/1').then(r => r.json()), fetch('/api/posts?userId=1').then(r => r.json()) ]); // Обидва запити йдуть паралельно; якщо один впав, результату немає

Використовуй коли всі операції мають успішно завершитись перед тим як продовжити роботу.

Promise.allSettled()

Те саме паралельне виконання, але чекає поки кожен проміс завершиться незалежно від результату. Повертає об'єкт для кожного: {status: 'fulfilled', value: ...} або {status: 'rejected', reason: ...}.

javascript
const results = await Promise.allSettled([fetchUser(), fetchPosts()]); const loaded = results .filter(r => r.status === 'fulfilled') .map(r => r.value);

Підходить коли частковий успіх прийнятний: дашборд, що показує ті дані, які вдалось завантажити.

Promise.race()

Повертає перший проміс, що завершився, незалежно від того, успішно чи з помилкою. Решта ігноруються. Найпоширеніший кейс — патерн timeout:

javascript
const withTimeout = (promise, ms) => Promise.race([ promise, new Promise((_, reject) => setTimeout(() => reject('Timeout'), ms)) ]);

Якщо всі вхідні проміси залишаються в стані pending, Promise.race() теж залишається pending назавжди.

Promise.any()

Повертає перший проміс, що завершився успішно. Відхилення пропускаються. Якщо всі відхиляються, кидає AggregateError. З'явився в ES2021, тому перевір чи потрібен поліфіл для старих середовищ.

javascript
// Використовуємо той CDN, що відповів першим const resource = await Promise.any([ fetch('https://cdn1.example.com/lib.js'), fetch('https://cdn2.example.com/lib.js'), fetch('https://cdn3.example.com/lib.js') ]);

Як це працює всередині

Коли resolve() або reject() спрацьовує, Promise позначає себе завершеним і додає обробники .then() або .catch() до черги мікрозавдань (microtask queue). Мікрозавдання виконуються після поточного синхронного коду, але до того як браузер перемалює сторінку або спрацює наступний setTimeout. Тому Promise.resolve().then(fn) завжди виконується раніше setTimeout(fn, 0): різні черги, різний пріоритет.

Типові помилки

1. Відсутній return в .then() з фігурними дужками

javascript
// Неправильно - результат губиться, наступний .then() отримує undefined fetch('/api/data') .then(response => { response.json(); // немає return }) .then(data => console.log(data)); // undefined // Правильно fetch('/api/data') .then(response => response.json()) .then(data => console.log(data));

Стрілкова функція з {} не повертає значення автоматично.

2. Відсутній .catch() в кінці ланцюжка

javascript
// Неправильно - помилки зникають, застосунок продовжує роботу в зламаному стані fetch('/api').then(r => r.json()).then(showData); // Правильно fetch('/api').then(r => r.json()).then(showData).catch(console.error);

3. Очікування, що Promise.all() чекає всіх перед відхиленням

На практиці це помилка, яку я бачив найчастіше на код-рев'ю. Promise.all() зупиняється при першому відхиленні і не чекає повільніших промісів.

javascript
Promise.all([ Promise.resolve('OK'), Promise.reject('BOOM'), // відхиляється одразу slowPromise // результат ігнорується ]).catch(err => console.log(err)); // "BOOM" миттєво

Якщо потрібні всі результати, використовуй Promise.allSettled().

4. Обгортання в new Promise() коду, що вже повертає Promise

javascript
// Неправильно - зайві накладні витрати function loadUser() { return new Promise(resolve => resolve(fetch('/api/user'))); } // Правильно - fetch вже повертає Promise function loadUser() { return fetch('/api/user'); }

Де використовується

  • React useEffect: fetch('/api/data').then(r => r.json()).then(setData).catch(setError)
  • Express middleware: Promise.all([db.query(...), cache.get(...)]) для паралельного отримання даних
  • Redux Toolkit: createAsyncThunk керує lifecycle Promise всередині
  • Next.js getServerSideProps: Promise.all([fetchUser(), fetchPosts()]) для паралельних SSR-запитів

Питання на співбесіді

Q: Яка різниця між Promise.all() і Promise.allSettled()?
A: Promise.all() відхиляється, як тільки один проміс відхиляється. Promise.allSettled() чекає завершення всіх і повертає об'єкт для кожного з полями status та value або reason. Раннього виходу немає.

Q: Чому Promise.resolve().then(fn) виконується раніше setTimeout(fn, 0)?
A: Callback .then() потрапляє до черги мікрозавдань. setTimeout потрапляє до черги макрозавдань. Після кожного завдання рушій виконує всі мікрозавдання перед тим як узяти наступне макрозавдання. Мікрозавдання йдуть першими.

Q: Що повертає Promise.all([]) при порожньому масиві?
A: Одразу виконується з []. Немає промісів, немає чого чекати.

Q: Чи може Promise виконатись з іншим Promise як значенням?
A: Так. Якщо викликати resolve(anotherPromise), зовнішній Promise приймає стан внутрішнього і чекає поки він завершиться. Будь-який об'єкт з методом .then() (так зване "thenable") поводиться так само.

Q: Що повертає Promise.race() якщо всі вхідні проміси назавжди залишаються в pending?
A: Він теж залишається в pending назавжди. Нічого його не завершить.

Приклади

Завантаження профілю користувача з обробкою помилок

javascript
function loadUserProfile(userId) { return fetch(`/api/users/${userId}`) .then(response => { if (!response.ok) throw new Error('Користувача не знайдено'); return response.json(); }) .catch(error => { console.error('Помилка завантаження:', error.message); return null; // graceful fallback }); } loadUserProfile(1).then(profile => { if (profile) renderProfile(profile); });

Throw всередині .then() працює так само як reject(): помилка потрапляє до найближчого .catch().

Паралельні запити для дашборду

javascript
async function loadDashboard(userId) { try { const [user, posts, notifications] = await Promise.all([ fetch(`/api/users/${userId}`).then(r => r.json()), fetch(`/api/posts?author=${userId}`).then(r => r.json()), fetch(`/api/notifications/${userId}`).then(r => r.json()) ]); return { user, posts, notifications }; } catch (error) { console.error('Дашборд не завантажився:', error); return null; } }

Три запити відправляються одночасно. Загальний час дорівнює часу найповільнішого, а не сумі всіх трьох.

Крайній випадок з Promise.all та відхиленням

javascript
const tasks = [ Promise.resolve('Швидкий'), new Promise(resolve => setTimeout(() => resolve('Повільний'), 100)), Promise.reject('БАХ!'), // відхиляється одразу new Promise(resolve => setTimeout(() => resolve('Ніколи не буде'), 200)) ]; Promise.all(tasks) .then(results => console.log(results)) .catch(err => console.log('Провалено:', err)); // "Провалено: БАХ!" - без затримки

Proміс на 200мс ніколи не доставляє результат. Promise.all() вже відхилено. Якщо потрібні всі результати, заміни на Promise.allSettled() і фільтруй за полем status.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.

Дочитали статтю?