Що таке ланцюжок промісів у JavaScript
Promise chaining (ланцюжок промісів) - це патерн, де кожен .then() повертає новий Promise, а наступний обробник у ланцюгу отримує результат попереднього як вхідні дані.
Теорія
TL;DR
- Як доміно: повалиш перше - кожне наступне спрацьовує автоматично.
- Кожен
.then()створює новий Promise, розв'язаний тим, що повертає твій обробник. - Помилки "спливають" вниз по ланцюгу до найближчого
.catch()- він один на весь потік. - Для лінійних async-кроків (запит, парсинг, обробка) - чіпляй
.then(). Для циклів і розгалужень -async/awaitчитабельніший. async/await- це синтаксичний цукор над promise chaining, не заміна.
Швидкий приклад
fetch('https://jsonplaceholder.typicode.com/users/1')
.then(response => response.json()) // повертає Promise з об'єктом користувача
.then(user => `Привіт, ${user.name}!`) // повертає Promise з рядком
.then(greeting => console.log(greeting)) // виводить: "Привіт, Leanne Graham!"
.catch(err => console.error(err)); // ловить будь-яку помилку в ланцюгуКожен .then() передає результат наступному обробнику. Якщо будь-який крок падає з помилкою, виконання пропускає всі наступні .then() і одразу переходить до .catch().
Як ланцюг працює всередині
Коли ти викликаєш .then(handler), JavaScript створює новий Promise і ставить handler у чергу мікрозадач (microtask queue). Коли попередній Promise завершується, рушій бере задачу з черги, запускає обробник і розв'язує новий Promise тим, що той повернув.
Якщо обробник повернув просте значення - воно автоматично обгортається в розв'язаний Promise. Якщо повернув інший Promise - ланцюг чекає на його завершення. Якщо кинув помилку - новий Promise відхиляється.
Все це відбувається в черзі мікрозадач, яку event loop виконує до будь-яких setTimeout або setInterval. Ланцюг із 5 синхронних кроків завершиться повністю до того, як спрацює будь-який таймер.
Головна різниця від вкладених колбеків
Без ланцюгування - вкладаєш. Саме в цьому проблема.
// вкладено - callback hell
fetch('/user').then(response => {
response.json().then(user => {
fetch(`/posts/${user.id}`).then(res => {
res.json().then(posts => console.log(posts));
});
});
});
// ланцюжок - плоский і читабельний
fetch('/user')
.then(res => res.json())
.then(user => fetch(`/posts/${user.id}`))
.then(res => res.json())
.then(posts => console.log(posts))
.catch(err => console.error(err));Та сама логіка, зовсім інша читабельність. У ланцюговому варіанті ще й усі помилки обробляє один .catch().
Коли використовувати
- 2-4 лінійних async-кроки (запит, парсинг, трансформація, відповідь) - чіпляй
.then(). - Один обробник помилок на весь потік - один
.catch()в кінці. - Мікс синхронних і асинхронних повернень в одному потоці - ланцюг впорається з обома автоматично.
- Паралельні операції - краще
Promise.all(). - Умовні розгалуження або цикли -
async/awaitтут чистіший.
Типові помилки
Помилка 1: забуваєш повертати значення з .then()
// неправильно - повертає undefined, значення губиться
fetch('/user')
.then(res => res.json())
.then(user => { user.name; }) // немає return
.then(name => console.log(name)); // виведе: undefined
// правильно
fetch('/user')
.then(res => res.json())
.then(user => user.name) // неявний return зі стрілочної функції
.then(name => console.log(name)); // виведе: "Leanne Graham"Помилка 2: .catch() посередині ланцюга "ковтає" помилки
// неправильно - перший .catch() розв'язує ланцюг із undefined
fetch('/user')
.catch(err => console.log('помилка')) // ловить, але не перекидає
.then(data => console.log(data)); // виконується навіть при помилці, виводить: undefined
// правильно - перекинь помилку якщо потрібно поширити далі
fetch('/user')
.catch(err => { console.log(err); throw err; })
.then(data => console.log(data));Помилка 3: вкладаєш .then() замість плоского ланцюга
// неправильно - вкладено, ламає структуру
fetch('/user').then(res => {
return res.json().then(user => fetch(`/posts/${user.id}`));
});
// правильно - повертай внутрішній проміс, нехай ланцюг продовжується
fetch('/user')
.then(res => res.json())
.then(user => fetch(`/posts/${user.id}`))
.then(res => res.json());Помилка 4: не знаєш, що .then() автоматично обгортає синхронні значення
Promise.resolve(5)
.then(x => x * 2) // повертає Promise<10>, а не число 10
.then(y => console.log(y)); // виведе: 10Це правильна поведінка, не баг. Але це важливо для часових меж: код поза ланцюгом не побачить 10 синхронно, навіть якщо математика виконується миттєво.
Де зустрічається в реальних проєктах
- Express.js: обробники маршрутів -
fetchUser().then(validate).then(respond).catch(errorHandler). - React
useEffect: ланцюгfetchдля послідовного завантаження користувача, потім його постів. - Axios інтерсептори: запит, трансформація, відповідь як вбудований ланцюг.
- Node.js
fs.promises:readFile().then(parse).then(writeFile). - Помилку з
.catch()посередині ланцюга я бачив у продакшені не один раз. Вона тиха - проявляється лише коли перший запит реально падає.
Питання на співбесіді
Q: Що станеться якщо обробник у .then() кине помилку?
A: Promise, повернутий цим .then(), відхилиться з тією помилкою. Виконання пропустить всі наступні .then() і перейде до найближчого .catch() в ланцюгу.
Q: Можна мати кілька .catch() в одному ланцюгу?
A: Так. Але перший .catch() поглинає відхилення і розв'язує ланцюг своїм значенням, якщо ти не перекидаєш помилку через throw. Наступні .catch() не спрацюють без явного перекидання.
Q: Яка різниця між поверненням простого значення і Promise із .then()?
A: Ззовні - ніякої. Просте значення автоматично обгортається в розв'язаний Promise. Повернутий Promise змушує ланцюг зачекати на його завершення. В обох випадках наступний .then() отримує вже розв'язане значення.
Q: Як promise chaining пов'язаний з event loop?
A: Кожен .then() ставить мікрозадачу в чергу. Event loop обробляє всі мікрозадачі до переходу до макрозадач типу setTimeout. Ланцюг із 5 синхронних кроків виконається повністю до того, як спрацює будь-який таймер.
Q: Коли краще async/await замість ланцюга?
A: Коли є умовна логіка, цикли по async-операціях або потрібна обробка у стилі try/catch. Ланцюги важко читати, як тільки всередині .then() з'являється if. Для простого лінійного потоку - ланцюг нормально, іноді навіть коротший.
Q: (Senior) Що V8 робить коли обробник у .then() повертає інший Promise?
A: V8 не передає повернутий Promise як значення напряму. Він запускає Promise resolution procedure: викликає .then() на внутрішньому Promise і з'єднує його завершення з зовнішнім. Ланцюг чекає і отримує значення внутрішнього Promise, а не сам об'єкт Promise.
Приклади
Базовий: отримати користувача за ID
fetch('https://jsonplaceholder.typicode.com/users/1')
.then(response => {
if (!response.ok) throw new Error('Не знайдено');
return response.json(); // парсимо JSON, повертає Promise
})
.then(user => {
console.log(user.name); // виводить: "Leanne Graham"
return user.id;
})
.then(id => console.log(`User ID: ${id}`)) // виводить: "User ID: 1"
.catch(err => console.error('Помилка:', err.message));Кожен крок передає значення вперед. Якщо response.ok виявиться false, кинута помилка пропустить всі наступні обробники і потрапить у .catch().
Середній: маршрут Express з ланцюгом запитів до БД
app.get('/user/:id', (req, res) => {
fetchUser(req.params.id)
.then(user => fetchUserPosts(user.id)) // отримуємо пов'язані дані
.then(posts => res.json(posts)) // відповідаємо масивом постів
.catch(err => res.status(500).json({ error: err.message }));
});Один .catch() покриває обидва запити. Якщо fetchUser впаде, fetchUserPosts не виконається і одразу відправиться відповідь із помилкою.
Просунутий: повернення Promise зсередини .then()
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
delay(100)
.then(() => {
return delay(200).then(() => 'готово'); // ланцюг зачекає на внутрішній проміс
})
.then(result => console.log(result)); // виведе: "готово" приблизно через 300мсКоли ти повертаєш Promise із .then(), зовнішній ланцюг не продовжується, поки той Promise не розв'яжеться. Результатом стає значення внутрішнього Promise, а не сам об'єкт Promise. Саме цей механізм робить послідовні fetch-запити в ланцюгу коректними.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.