Skip to main content

Callback hell

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

Теорія

TL;DR

  • Причина: послідовні асинхронні кроки, де кожен наступний потребує результату попереднього.
  • Симптом: код зсувається вправо «драбинкою», рівень вкладеності росте з кожним кроком.
  • Наслідки: погана читабельність, дублювання обробки помилок, неможливість перевикористати окремий крок.
  • Проблема не в колбеках як таких, а в глибокій вкладеності і некерованому потоці виконання.
  • Лікування: Promise і async/await, які записують ту саму послідовність лінійно.

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

javascript
getUser(id, user => { getPosts(user.id, posts => { getComments(posts[0].id, comments => { saveToFile(comments, () => { console.log('Готово!'); }); }); }); });

Чотири кроки, і вже чотири рівні вкладеності. Додайте п'ятий, і читати це стане ще важче.

Чому з'являється вкладеність

Callback не повертає значення викликачу: результат приходить пізніше, всередину переданої функції. Тому єдиний спосіб використати цей результат у наступному кроці, це написати наступний виклик усередині колбека. Кожна залежність між кроками додає рівень вкладеності, і послідовність з N операцій дає N рівнів відступів.

Так лінійна за змістом логіка («візьми користувача, потім його пости, потім коментарі, потім збережи») перетворюється на пірамідальну за формою. Цю форму і називають pyramid of doom.

Чим це шкідливо

  • Читабельність. Щоб зрозуміти порядок кроків, треба читати структуру вкладеності, а не послідовність рядків. Кінець кожного кроку губиться серед закриваючих дужок.
  • Обробка помилок. Немає спільного місця для збоїв: у стилі error-first кожен рівень має власний параметр err, і перевірку доводиться повторювати знову й знову.
  • Зміни і перевикористання. Вирізати один крок або переставити кроки місцями важко: вони фізично вкладені один в одний, а не стоять поруч.
javascript
getUser(id, (err, user) => { if (err) return handle(err); getPosts(user.id, (err, posts) => { if (err) return handle(err); getComments(posts[0].id, (err, comments) => { if (err) return handle(err); // корисна робота ховається під трьома перевірками }); }); });

Як розплутати: Promise і async/await

Promise повертає значення викликачу, тому наступний крок можна поставити поруч, а не всередині. Ланцюжок .then уже робить код пласким, а async/await дає майже синхронний вигляд:

javascript
const user = await getUser(id); const posts = await getPosts(user.id); const comments = await getComments(posts[0].id); await saveToFile(comments);

Той самий код з обробкою помилок в одному місці:

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

Якщо кроки незалежні, їх не треба вишиковувати послідовно: Promise.all запускає їх паралельно і чекає всі одразу.

javascript
const [user, settings] = await Promise.all([getUser(id), getSettings(id)]);

Коли API старе і вміє тільки колбеки, його загортають у Promise один раз (util.promisify у Node.js або власна обгортка), і далі весь код пишеться лінійно.

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

  • Вважати, що колбеки самі по собі погані. Погана саме глибока вкладеність; map, forEach і обробники подій це теж колбеки, і з ними все гаразд.
  • Обробляти помилку тільки на найглибшому рівні: збій на першому кроці просто загубиться.
  • «Виправляти» вкладеність винесенням кожного колбека в іменовану функцію: рівнів стає менше візуально, але потік виконання все одно розсипаний по файлу.
  • Ставити await у циклі для незалежних операцій: послідовність стає повільнішою там, де мав бути Promise.all.
  • Забувати, що всередині колбека return повертає значення не з зовнішньої функції, а з самого колбека.

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

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

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