Skip to main content

Що таке callback hell

Callback hell - це ситуація, коли в коді використовується багато вкладених одна в одну callback-функцій. Через це код стає важко читати, важко підтримувати і практично неможливо налагоджувати: callback викликають інші callback і утворюють «піраміду жаху».

Теорія

TL;DR

  • Callback hell виникає, коли кожна наступна асинхронна операція запускається всередині callback попередньої.
  • Візуально це піраміда: відступи ростуть праворуч, а закривні дужки збираються у довгий хвіст.
  • Головні проблеми: погана читабельність, дубльована обробка помилок, жорсткий зв'язок кроків, неінформативний stack trace.
  • Проблема характерна для асинхронного JavaScript часів до ES6: XMLHttpRequest, setTimeout, addEventListener, ранній Node.js.
  • Лікується декомпозицією на іменовані функції, а повністю - переходом на Promise і async/await.

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

javascript
// класична «піраміда приреченості» getUser('tim', (user) => { getPosts(user.id, (posts) => { getComments(posts[0].id, (comments) => { saveToDB(comments, (result) => { console.log('Готово!'); }); }); }); });

Тут кожна функція чекає на завершення попередньої, callback-функції вкладені одна в одну, відступи ростуть праворуч, помилки складно ловити, а код перетворюється на «ліс» із дужок. Це і є callback hell.

Чому це погано

ПроблемаОпис
Погана читабельністьРівні вкладеності роблять код візуально важким
Складно обробляти помилкиКожному callback потрібна власна перевірка if (err) ...
Залежності ланцюжкомКожна операція чекає на попередню
Складне налагодженняВинятки губляться, stack trace довгий і нічого не пояснює
Код неможливо перевикористатиУся логіка «зашита» у вкладені анонімні функції

Звідки він береться

Проблема характерна для асинхронного JavaScript часів до ES6, коли все будувалося на callback:

  • робота з мережею: XMLHttpRequest;
  • таймери: setTimeout;
  • події: addEventListener;
  • Node.js до появи Promise API.

Ось як виглядав типовий старий асинхронний код:

javascript
fs.readFile('user.json', (err, data) => { if (err) throw err; getPosts(JSON.parse(data).id, (err, posts) => { if (err) throw err; sendEmail(posts, (err) => { if (err) throw err; console.log('Лист надіслано!'); }); }); });

Усе працює, але виглядає жахливо: такий код важко підтримувати, тестувати й розширювати.

Як уникнути callback hell

1. Іменовані функції

Найдешевший крок: винести кожен callback в окрему функцію з іменем. Це хоча б «випрямляє» код.

javascript
function handleUser(user) { getPosts(user.id, handlePosts); } function handlePosts(posts) { getComments(posts[0].id, handleComments); } function handleComments(comments) { console.log('Готово!'); } getUser('tim', handleUser);

2. Promise

javascript
getUser('tim') .then(getPosts) .then(getComments) .then(saveToDB) .then(() => console.log('Готово!')) .catch(console.error);

Код став лінійним, читається згори вниз, без вкладеності, а одного .catch() вистачає на весь ланцюжок.

3. async / await

javascript
async function run() { try { const user = await getUser('tim'); const posts = await getPosts(user.id); const comments = await getComments(posts[0].id); await saveToDB(comments); console.log('Готово!'); } catch (err) { console.error(err); } } run();

Тепер код виглядає як синхронний, але працює асинхронно. Жодних пірамід.

Проста аналогія:

Callback hell - це наче готувати вечерю, де кожен крок вимагає дзвінка другові, той дзвонить ще комусь, і всі чекають одне на одного, поки не виросте «піраміда дзвінків».

Підсумок

ТермінОпис
Callback hellСитуація з глибокою вкладеністю callback-функцій
ПричинаАсинхронний код, побудований на callback
НедолікиПогана читабельність, складне налагодження, дублювання логіки
РішенняPromise, async/await, іменовані функції, декомпозиція

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

  • Думати, що callback hell - це про обсяг коду. Проблема саме у вкладеності та жорсткому зв'язку кроків, а не в кількості рядків.
  • Переписати піраміду на Promise, але залишити вкладеність: викликати новий .then() усередині попереднього замість того, щоб повернути Promise назовні.
  • Забути return у .then(): наступний крок отримає undefined, і ланцюжок мовчки «розсиплеться».
  • Обробляти помилку в кожному callback окремо замість одного .catch() або одного try/catch навколо await.
  • Ставити await у циклі там, де операції незалежні: лінійний код стає повільним, тут потрібен Promise.all.
  • Вважати, що async/await прибирає callback. Він їх лише ховає: під капотом усе одно Promise і черга мікрозадач.

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

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

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