Представь интернет-магазин: пользователь нажимает «Оплатить», и сайт обращается к платёжному шлюзу. Ответ может прийти через 300ms или через 3 секунды. Если бы браузер просто «ждал» — страница бы зависла, кнопки перестали реагировать, пользователь бы закрыл вкладку. Колбэки — первое решение этой проблемы.
JavaScript однопоточный: в один момент выполняется только одна операция. Для операций, которые занимают время (сеть, файлы, таймеры), нужен механизм «запусти и вызови меня когда готово». Этот механизм — колбэк-функция.
// Синхронно — каждая строка ждёт предыдущую
console.log('1. Начало')
console.log('2. Проверка корзины')
console.log('3. Конец')
// Вывод: 1, 2, 3 — строго по порядку
// Асинхронно — setTimeout не блокирует поток
console.log('1. Запрос к API')
setTimeout(() => console.log('2. Ответ от API'), 1000)
console.log('3. Показываем спиннер')
// Вывод: 1, 3, 2 — спиннер показывается сразу, не ожидая ответаfunction processPayment(amount, onSuccess, onError) {
// симуляция запроса к платёжному шлюзу
setTimeout(() => {
if (amount > 0) {
onSuccess({ transactionId: 'TXN-001', amount })
} else {
onError(new Error('Некорректная сумма'))
}
}, 500)
}
processPayment(
1500,
result => console.log('Оплачено:', result.transactionId), // колбэк успеха
err => console.log('Ошибка:', err.message) // колбэк ошибки
)Соглашение: первый аргумент колбэка — ошибка (null если всё ОК), второй — данные:
function fetchOrder(orderId, callback) {
setTimeout(() => {
if (orderId > 0) callback(null, { id: orderId, status: 'delivered' })
else callback(new Error('Заказ не найден'))
}, 100)
}
fetchOrder(42, (err, order) => {
if (err) return console.log('Ошибка:', err.message)
console.log('Заказ:', order.status) // 'Заказ: delivered'
})Когда операции зависят друг от друга, колбэки вкладываются — код уходит вправо:
// Получить пользователя → его заказы → детали заказа → доставку
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)
getDelivery(details.deliveryId, (err, delivery) => {
if (err) return handleError(err)
console.log('Статус доставки:', delivery.status)
// Ещё уровень? Становится нечитаемым...
})
})
})
})Проблемы callback hell: код уходит вправо, ошибку нужно обрабатывать на каждом уровне, сложно добавить новый шаг, трудно читать.
// То же самое с async/await — читается как синхронный код
async function getDeliveryStatus(userId) {
try {
const user = await getUser(userId)
const orders = await getOrders(user.id)
const details = await getOrderDetails(orders[0].id)
const delivery = await getDelivery(details.deliveryId)
console.log('Статус:', delivery.status)
} catch (err) {
handleError(err) // одна точка для всех ошибок
}
}JS-движок постоянно проверяет очередь задач. Когда стек вызовов пуст — берёт следующую задачу из очереди. Именно так колбэки из setTimeout, fetch и событий попадают в выполнение после завершения текущего кода.
Ошибка 1: забыли return после обработки ошибки
// Неправильно — код продолжает выполняться после ошибки!
fetchUser(id, (err, user) => {
if (err) console.log('Ошибка:', err.message)
console.log(user.name) // CRASH — user может быть undefined
})
// Правильно
fetchUser(id, (err, user) => {
if (err) return console.log('Ошибка:', err.message) // return!
console.log(user.name)
})Ошибка 2: вызов колбэка несколько раз
// Неправильно — callback вызывается дважды при ошибке
function loadData(url, cb) {
if (!url) {
cb(new Error('URL не задан'))
// забыли return — код продолжает выполняться
}
fetch(url).then(r => r.json()).then(data => cb(null, data))
}
// Правильно
function loadData(url, cb) {
if (!url) return cb(new Error('URL не задан'))
fetch(url).then(r => r.json()).then(data => cb(null, data))
}Ошибка 3: синхронный колбэк в асинхронном API
let result
fetchData((err, data) => {
result = data // данные придут позже!
})
console.log(result) // undefined — данные ещё не пришлиCallback hell vs промисы — загрузка данных пользователя из интернет-магазина
// Симуляция асинхронных запросов к API магазина
function delay(ms, data, fail = false) {
return new Promise((resolve, reject) =>
setTimeout(() => fail ? reject(new Error('Сеть недоступна')) : resolve(data), ms)
)
}
// === Callback hell: загрузить пользователя → заказы → статус доставки ===
function loadWithCallbacks(userId) {
// Имитация: получаем пользователя
setTimeout((err, user) => {
err = null; user = { id: userId, name: 'Иван' }
if (err) return console.error('Ошибка пользователя:', err)
// Имитация: получаем заказы
setTimeout((err2, orders) => {
err2 = null; orders = [{ id: 1, userId, total: 1500 }]
if (err2) return console.error('Ошибка заказов:', err2)
// Имитация: получаем доставку
setTimeout(() => {
const delivery = { orderId: 1, status: 'В пути' }
console.log(`[${user.name}] Заказ #${orders[0].id}: ${delivery.status}`)
// Если нужно ещё — ещё один уровень вложенности...
}, 100)
}, 100)
}, 100)
}
// === Промисы: плоская читаемая цепочка ===
async function loadWithPromises(userId) {
try {
const user = await delay(100, { id: userId, name: 'Иван' })
const orders = await delay(100, [{ id: 1, userId, total: 1500 }])
const delivery = await delay(100, { orderId: 1, status: 'В пути' })
console.log(`[${user.name}] Заказ #${orders[0].id}: ${delivery.status}`)
// '[Иван] Заказ #1: В пути'
} catch (err) {
console.error('Ошибка на любом шаге:', err.message)
}
}
loadWithPromises(42)
// === Практический пример: repeat — вызов функции N раз с паузой ===
function repeat(fn, times, delayMs) {
if (times <= 0) return
setTimeout(() => {
fn()
repeat(fn, times - 1, delayMs) // рекурсивный вызов
}, delayMs)
}
let tick = 0
repeat(() => {
tick++
console.log(`Tick ${tick}`)
}, 3, 200)
// Через 200ms: 'Tick 1'
// Через 400ms: 'Tick 2'
// Через 600ms: 'Tick 3'Представь интернет-магазин: пользователь нажимает «Оплатить», и сайт обращается к платёжному шлюзу. Ответ может прийти через 300ms или через 3 секунды. Если бы браузер просто «ждал» — страница бы зависла, кнопки перестали реагировать, пользователь бы закрыл вкладку. Колбэки — первое решение этой проблемы.
JavaScript однопоточный: в один момент выполняется только одна операция. Для операций, которые занимают время (сеть, файлы, таймеры), нужен механизм «запусти и вызови меня когда готово». Этот механизм — колбэк-функция.
// Синхронно — каждая строка ждёт предыдущую
console.log('1. Начало')
console.log('2. Проверка корзины')
console.log('3. Конец')
// Вывод: 1, 2, 3 — строго по порядку
// Асинхронно — setTimeout не блокирует поток
console.log('1. Запрос к API')
setTimeout(() => console.log('2. Ответ от API'), 1000)
console.log('3. Показываем спиннер')
// Вывод: 1, 3, 2 — спиннер показывается сразу, не ожидая ответаfunction processPayment(amount, onSuccess, onError) {
// симуляция запроса к платёжному шлюзу
setTimeout(() => {
if (amount > 0) {
onSuccess({ transactionId: 'TXN-001', amount })
} else {
onError(new Error('Некорректная сумма'))
}
}, 500)
}
processPayment(
1500,
result => console.log('Оплачено:', result.transactionId), // колбэк успеха
err => console.log('Ошибка:', err.message) // колбэк ошибки
)Соглашение: первый аргумент колбэка — ошибка (null если всё ОК), второй — данные:
function fetchOrder(orderId, callback) {
setTimeout(() => {
if (orderId > 0) callback(null, { id: orderId, status: 'delivered' })
else callback(new Error('Заказ не найден'))
}, 100)
}
fetchOrder(42, (err, order) => {
if (err) return console.log('Ошибка:', err.message)
console.log('Заказ:', order.status) // 'Заказ: delivered'
})Когда операции зависят друг от друга, колбэки вкладываются — код уходит вправо:
// Получить пользователя → его заказы → детали заказа → доставку
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)
getDelivery(details.deliveryId, (err, delivery) => {
if (err) return handleError(err)
console.log('Статус доставки:', delivery.status)
// Ещё уровень? Становится нечитаемым...
})
})
})
})Проблемы callback hell: код уходит вправо, ошибку нужно обрабатывать на каждом уровне, сложно добавить новый шаг, трудно читать.
// То же самое с async/await — читается как синхронный код
async function getDeliveryStatus(userId) {
try {
const user = await getUser(userId)
const orders = await getOrders(user.id)
const details = await getOrderDetails(orders[0].id)
const delivery = await getDelivery(details.deliveryId)
console.log('Статус:', delivery.status)
} catch (err) {
handleError(err) // одна точка для всех ошибок
}
}JS-движок постоянно проверяет очередь задач. Когда стек вызовов пуст — берёт следующую задачу из очереди. Именно так колбэки из setTimeout, fetch и событий попадают в выполнение после завершения текущего кода.
Ошибка 1: забыли return после обработки ошибки
// Неправильно — код продолжает выполняться после ошибки!
fetchUser(id, (err, user) => {
if (err) console.log('Ошибка:', err.message)
console.log(user.name) // CRASH — user может быть undefined
})
// Правильно
fetchUser(id, (err, user) => {
if (err) return console.log('Ошибка:', err.message) // return!
console.log(user.name)
})Ошибка 2: вызов колбэка несколько раз
// Неправильно — callback вызывается дважды при ошибке
function loadData(url, cb) {
if (!url) {
cb(new Error('URL не задан'))
// забыли return — код продолжает выполняться
}
fetch(url).then(r => r.json()).then(data => cb(null, data))
}
// Правильно
function loadData(url, cb) {
if (!url) return cb(new Error('URL не задан'))
fetch(url).then(r => r.json()).then(data => cb(null, data))
}Ошибка 3: синхронный колбэк в асинхронном API
let result
fetchData((err, data) => {
result = data // данные придут позже!
})
console.log(result) // undefined — данные ещё не пришлиCallback hell vs промисы — загрузка данных пользователя из интернет-магазина
// Симуляция асинхронных запросов к API магазина
function delay(ms, data, fail = false) {
return new Promise((resolve, reject) =>
setTimeout(() => fail ? reject(new Error('Сеть недоступна')) : resolve(data), ms)
)
}
// === Callback hell: загрузить пользователя → заказы → статус доставки ===
function loadWithCallbacks(userId) {
// Имитация: получаем пользователя
setTimeout((err, user) => {
err = null; user = { id: userId, name: 'Иван' }
if (err) return console.error('Ошибка пользователя:', err)
// Имитация: получаем заказы
setTimeout((err2, orders) => {
err2 = null; orders = [{ id: 1, userId, total: 1500 }]
if (err2) return console.error('Ошибка заказов:', err2)
// Имитация: получаем доставку
setTimeout(() => {
const delivery = { orderId: 1, status: 'В пути' }
console.log(`[${user.name}] Заказ #${orders[0].id}: ${delivery.status}`)
// Если нужно ещё — ещё один уровень вложенности...
}, 100)
}, 100)
}, 100)
}
// === Промисы: плоская читаемая цепочка ===
async function loadWithPromises(userId) {
try {
const user = await delay(100, { id: userId, name: 'Иван' })
const orders = await delay(100, [{ id: 1, userId, total: 1500 }])
const delivery = await delay(100, { orderId: 1, status: 'В пути' })
console.log(`[${user.name}] Заказ #${orders[0].id}: ${delivery.status}`)
// '[Иван] Заказ #1: В пути'
} catch (err) {
console.error('Ошибка на любом шаге:', err.message)
}
}
loadWithPromises(42)
// === Практический пример: repeat — вызов функции N раз с паузой ===
function repeat(fn, times, delayMs) {
if (times <= 0) return
setTimeout(() => {
fn()
repeat(fn, times - 1, delayMs) // рекурсивный вызов
}, delayMs)
}
let tick = 0
repeat(() => {
tick++
console.log(`Tick ${tick}`)
}, 3, 200)
// Через 200ms: 'Tick 1'
// Через 400ms: 'Tick 2'
// Через 600ms: 'Tick 3'Реализуй функцию retry(fn, times, delay) для системы мониторинга: она вызывает асинхронную функцию fn (error-first колбэк), и если та завершилась с ошибкой — повторяет попытку через delay миллисекунд, но не более times раз. При успехе или исчерпании попыток вызывает финальный колбэк.
В случае ошибки передай в callback: callback(err). При повторной попытке уменьши times: retry(fn, times - 1, delay, callback). Базовый случай: times <= 1 означает что это последняя попытка.