Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Що таке ланцюжок промісів у JavaScript». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Promise chaining (ланцюжок промісів)** - це патерн, де кожен `.then()` повертає новий Promise і передає своє значення наступному обробнику. ```javascript fetch('/api/user') .then(res => res.json()) .then(user => user.name) .then(name => console.log(name)) .catch(err => console.error(err)); ``` **Головне:** забудеш `return` всередині `.then()` - наступний обробник отримає `undefined`.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Promise chaining (ланцюжок промісів)** - це патерн, де кожен `.then()` повертає новий Promise, а наступний обробник у ланцюгу отримує результат попереднього як вхідні дані. ## Теорія ### TL;DR - Як доміно: повалиш перше - кожне наступне спрацьовує автоматично. - Кожен `.then()` створює новий Promise, розв'язаний тим, що повертає твій обробник. - Помилки "спливають" вниз по ланцюгу до найближчого `.catch()` - він один на весь потік. - Для лінійних async-кроків (запит, парсинг, обробка) - чіпляй `.then()`. Для циклів і розгалужень - [`async/await`](/questions/what-is-async-await-in-javascript) читабельніший. - `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](/questions/what-is-event-loop-in-javascript) виконує до будь-яких `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()`](/questions/what-is-promise-all-in-javascript). - Умовні розгалуження або цикли - `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`-запити в ланцюгу коректними.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.