Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Функції зворотного виклику та пекло зворотних викликів у JavaScript». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Callback (функція зворотного виклику)** - функція, яку передають як аргумент і викликають пізніше після завершення роботи. ```javascript [1, 2, 3].forEach(n => console.log(n)); // аргумент forEach - це callback setTimeout(() => console.log('готово'), 1000); // async callback, виконається через 1с ``` **Ключове:** ланцюжок з 3+ async callback-ів утворює callback hell - піраміду вкладеності з повторною обробкою помилок на кожному рівні. Вирішується через Promises або async/await.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Callback (функція зворотного виклику)** - функція, яку передають як аргумент до іншої функції, щоб та могла викликати її пізніше, зазвичай після завершення якоїсь роботи. ## Теорія ### TL;DR - Уяви, що залишаєш номер телефону в ресторані. Вони передзвонять, коли столик буде готовий, замість того щоб ти стояв у черзі. Ось як працює callback. - Callback-и бувають двох типів: синхронні (`.map()`, `.filter()`) виконуються одразу всередині методу; асинхронні (`setTimeout`, `fs.readFile`) виконуються після того, як поточний стек викликів звільниться. - Callback hell виникає, коли ланцюжок з 3+ async callback-ів утворює піраміду вкладеності. Обробка помилок повторюється на кожному рівні, а додавання кроку означає ще один рівень відступів. - Правило вибору: один async крок → callback підходить. Два і більше пов'язаних кроків → Promises або async/await. ### Швидкий приклад ```javascript function fetchUser(userId, callback) { setTimeout(() => { const user = { id: userId, name: 'Alice' }; callback(null, user); // null = помилки немає (error-first патерн) }, 1000); } fetchUser(123, (err, user) => { if (err) return console.error(err); console.log(user.name); // "Alice" - виводить через 1 секунду }); console.log('Це виконається першим'); // синхронний код не чекає ``` Зовнішній `console.log` спрацює одразу. Callback виконається через секунду: [event loop](/questions/event-loop) поставить його в чергу і запустить тільки після того, як стек викликів звільниться. ### Синхронні та асинхронні callback-и Не всі callback-и є асинхронними. `.map()` і `.filter()` викликають твою функцію синхронно, рядок за рядком всередині методу. `setTimeout` і `fs.readFile` інші: Node або браузер передають операцію фоновому API, а callback потрапляє в task queue. Event loop забере його звідти тільки після того, як поточний стек очиститься. Це питання часто з'являється на співбесідах: ```javascript setTimeout(() => console.log('A'), 0); console.log('B'); // Виведе: "B", потім "A" // Затримка 0мс не означає "виконати зараз". Означає "поставити в чергу". ``` ### Патерн помилка-першою У Node.js є конвенція: перший аргумент будь-якого callback-у завжди є помилкою. Якщо помилки немає - передають `null`. Це стандартизує обробку помилок по всіх async API. ```javascript fs.readFile('config.json', 'utf8', (err, data) => { if (err) { console.error('Файл не знайдено:', err.message); return; // завжди повертайся при помилці } const config = JSON.parse(data); console.log(config); }); ``` Найчастіша помилка в Node.js коді, яку я бачив - пропущена перевірка `err`. Отримуєш `undefined` у `data` без жодного натяку на те, що пішло не так. ### Callback hell Коли кожен async крок потребує результату попереднього, починається вкладеність. Після трьох рівнів код повзе вправо. Це і є callback hell, або піраміда загибелі. ```javascript // Ось як виглядав реальний код до появи Promises getUser(userId, (err, user) => { if (err) return handleError(err); getOrders(user.id, (err, orders) => { if (err) return handleError(err); getOrderDetails(orders[0].id, (err, details) => { if (err) return handleError(err); getShippingInfo(details.shipId, (err, shipping) => { if (err) return handleError(err); console.log(shipping); }); }); }); }); ``` Проблема не тільки у форматуванні. Обробка помилок дублюється на кожному рівні. Додати крок означає додати рівень вкладеності. Рефакторити будь-яку частину цього ланцюжка - повільно і ризиковано. ### Як вирішити callback hell Є три підходи. Кожен має своє місце. **Promises** розгортають ланцюжок у читабельну послідовність: ```javascript getUser(userId) .then(user => getOrders(user.id)) .then(orders => getOrderDetails(orders[0].id)) .then(details => getShippingInfo(details.shipId)) .then(shipping => console.log(shipping)) .catch(handleError); // один обробник для всього ланцюжка ``` **async/await** робить код схожим на синхронний: ```javascript async function getShipping(userId) { const user = await getUser(userId); const orders = await getOrders(user.id); const details = await getOrderDetails(orders[0].id); return getShippingInfo(details.shipId); } ``` **Іменовані функції** - швидке рішення без переходу на Promises. Виносиш callback-и назовні: ```javascript function onUser(err, user) { if (err) return handleError(err); getOrders(user.id, onOrders); } function onOrders(err, orders) { if (err) return handleError(err); getOrderDetails(orders[0].id, onDetails); } getUser(userId, onUser); // плоска структура, та сама поведінка ``` ### Коли використовувати callback-и - Одна async операція, наприклад один `addEventListener` або один `setTimeout`: callback підходить. - Ітерація масивів через `.map()`, `.filter()`, `.reduce()`: синхронні callback-и, проблем з вкладеністю немає. - Два і більше пов'язаних async кроків: переходь на [Promises](/questions/promises-in-javascript) або async/await. - Async операції в Node.js з можливими помилками: Promises дають один `.catch()` замість перевірки `if (err)` на кожному рівні. ### Типові помилки **Ігнорування параметра помилки:** ```javascript // Неправильно - data буде undefined, якщо файл відсутній, без жодної помилки fs.readFile('data.txt', (data) => console.log(data)); // Правильно fs.readFile('data.txt', 'utf8', (err, data) => { if (err) return console.error(err); console.log(data); }); ``` **Використання змінної до виконання callback-у:** ```javascript let user; fetchUser(1, (err, result) => { user = result; // встановлюється через ~500мс }); console.log(user); // undefined - цей рядок виконується до callback-у ``` Якщо потрібні дані - використовуй їх всередині callback-у або поверни Promise. **Хибне уявлення що `setTimeout(..., 0)` виконується синхронно:** ```javascript setTimeout(() => console.log('першим'), 0); console.log('другим'); // Виведе: "другим", потім "першим" ``` Нуль мілісекунд означає "поставити в чергу після очищення стеку", а не "виконати зараз". Це питання регулярно з'являється на співбесідах. **Рекурсивний polling без умови зупинки:** ```javascript // Виконується нескінченно function poll(cb) { setTimeout(() => poll(cb), 1000); } // Правильно: додай лічильник function poll(cb, count = 0) { if (count >= 10) return cb(); setTimeout(() => poll(cb, count + 1), 1000); } ``` ### Де зустрічається - Node.js/Express: `app.get('/users', (req, res) => { fs.readFile(..., callback) })` - Браузерні події: `button.addEventListener('click', handler)` - Методи масивів у будь-якому JS коді: `.map()`, `.filter()`, `.forEach()` - Застарілий jQuery AJAX: `$.get('/api/data', {}, callback)` - досі живий в старих проектах - `setTimeout` та `setInterval` для таймерів і polling ### Питання на співбесіді **Q:** Що таке callback-функція? **A:** Функція, яку передають як аргумент і яка викликається пізніше після завершення якоїсь роботи. `[1, 2, 3].forEach(n => console.log(n))` - найпростіший приклад. **Q:** Що таке callback hell і чому це проблема? **A:** Вкладеність з 3+ async callback-ів утворює піраміду. Обробка помилок дублюється на кожному рівні, додавання кроку означає новий рівень відступів, і код стає важким для рефакторингу. **Q:** Що таке патерн помилка-першою в Node.js? **A:** Конвенція, де перший аргумент callback-у завжди є помилкою (або `null` при успіху). Стандартизує обробку помилок - завжди перевіряй `err` перед тим, як використовувати дані. **Q:** В чому різниця між синхронними та асинхронними callback-ами? **A:** Синхронні (`.map()`, `.filter()`) виконуються одразу всередині функції, яка їх отримала. Асинхронні (`setTimeout`, `fs.readFile`) ставляться в чергу event loop-ом і виконуються після очищення поточного стеку. **Q:** Як event loop обробляє async callback-и? **A:** Node або браузер передають async роботу фоновому API. Після завершення callback потрапляє в task queue. Event loop переміщує його до стеку викликів тільки коли стек порожній, тому `setTimeout(..., 0)` все одно виконується після синхронного коду. **Q:** Чому синхронна рекурсія може викликати stack overflow, а async polling - ні? **A:** Синхронна рекурсія додає фрейм до стеку викликів з кожним викликом. Node обмежує стек приблизно до 10k фреймів. Async polling через `setTimeout` виносить кожну ітерацію в task queue, тому стек не росте. Кожен poll починається з чистого аркуша. ## Приклади ### Базовий async callback ```javascript function fetchUser(userId, callback) { setTimeout(() => { const user = { id: userId, name: 'Alice' }; callback(null, user); // null = помилки немає }, 1000); } fetchUser(123, (err, user) => { if (err) return console.error(err); console.log(user.name); // "Alice" - через 1 секунду }); ``` ### Маршрут Express.js з асинхронним читанням файлу ```javascript const fs = require('fs'); const express = require('express'); const app = express(); app.get('/user/:id', (req, res) => { fs.readFile(`users/${req.params.id}.json`, 'utf8', (err, data) => { if (err) return res.status(500).json({ error: 'Користувача не знайдено' }); res.json(JSON.parse(data)); // { "name": "Bob" } }); }); // GET /user/1 зчитує файл асинхронно і відповідає через ~10мс ``` Сам обробник маршруту - callback. `fs.readFile` callback - другий callback всередині. Два рівні ще читабельні. Ось де варто зупинитися. ### Callback hell проти async/await ```javascript // Callback hell: 3 пов'язаних async операції getUser(123, (err, user) => { if (err) return handleError(err); getPosts(user.id, (err, posts) => { if (err) return handleError(err); getComments(posts[0].id, (err, comments) => { if (err) return handleError(err); console.log(comments); }); }); }); // async/await: та сама логіка, без вкладеності async function loadComments(userId) { const user = await getUser(userId); const posts = await getPosts(user.id); const comments = await getComments(posts[0].id); return comments; // один try/catch охоплює весь ланцюжок } ``` Поведінка однакова. У версії з async/await одне місце для обробки помилок і зрозумілий порядок читання зверху вниз.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.