Що таке callback hell
Callback hell - це ситуація, коли в коді використовується багато вкладених одна в одну callback-функцій. Через це код стає важко читати, важко підтримувати і практично неможливо налагоджувати: callback викликають інші callback і утворюють «піраміду жаху».
Теорія
TL;DR
- Callback hell виникає, коли кожна наступна асинхронна операція запускається всередині callback попередньої.
- Візуально це піраміда: відступи ростуть праворуч, а закривні дужки збираються у довгий хвіст.
- Головні проблеми: погана читабельність, дубльована обробка помилок, жорсткий зв'язок кроків, неінформативний stack trace.
- Проблема характерна для асинхронного JavaScript часів до ES6:
XMLHttpRequest,setTimeout,addEventListener, ранній Node.js. - Лікується декомпозицією на іменовані функції, а повністю - переходом на Promise і async/await.
Швидкий приклад
// класична «піраміда приреченості»
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.
Ось як виглядав типовий старий асинхронний код:
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 в окрему функцію з іменем. Це хоча б «випрямляє» код.
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
getUser('tim')
.then(getPosts)
.then(getComments)
.then(saveToDB)
.then(() => console.log('Готово!'))
.catch(console.error);Код став лінійним, читається згори вниз, без вкладеності, а одного .catch() вистачає на весь ланцюжок.
3. async / await
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 і черга мікрозадач.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.