Skip to main content

Що таке ланцюжок промісів у JavaScript

Promise chaining (ланцюжок промісів) - це патерн, де кожен .then() повертає новий Promise, а наступний обробник у ланцюгу отримує результат попереднього як вхідні дані.

Теорія

TL;DR

  • Як доміно: повалиш перше - кожне наступне спрацьовує автоматично.
  • Кожен .then() створює новий Promise, розв'язаний тим, що повертає твій обробник.
  • Помилки "спливають" вниз по ланцюгу до найближчого .catch() - він один на весь потік.
  • Для лінійних async-кроків (запит, парсинг, обробка) - чіпляй .then(). Для циклів і розгалужень - async/await читабельніший.
  • async/await - це синтаксичний цукор над promise chaining, не заміна.

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

javascript
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 синхронних кроків завершиться повністю до того, як спрацює будь-який таймер.

Головна різниця від вкладених колбеків

Без ланцюгування - вкладаєш. Саме в цьому проблема.

javascript
// вкладено - 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()

javascript
// неправильно - повертає 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() посередині ланцюга "ковтає" помилки

javascript
// неправильно - перший .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() замість плоского ланцюга

javascript
// неправильно - вкладено, ламає структуру 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() автоматично обгортає синхронні значення

javascript
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

javascript
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 з ланцюгом запитів до БД

javascript
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()

javascript
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-запити в ланцюгу коректними.

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

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

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

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