Callback
Callback
Технический фундамент callback-контракта
У callback-функции должен быть предсказуемый контракт вызова:
- кто и когда вызывает callback;
- сколько раз callback может быть вызван;
- в каком формате передаются аргументы;
- что делать при ошибке.
Если этот контракт нефиксирован, появляются дубли вызова, гонки и трудноуловимые баги в асинхронном коде.
Смотри, что важно: в контракт callback часто включают еще один пункт — callback вызывается синхронно или асинхронно. Самый опасный вариант: "иногда сразу, иногда позже". Это ломает ожидания кода вокруг.
const cache = new Map();
function loadUserCached(id, callback) {
if (cache.has(id)) {
callback(null, cache.get(id)); // синхронно
return;
}
setTimeout(() => {
const user = { id, name: 'Мария' };
cache.set(id, user);
callback(null, user); // асинхронно
}, 0);
}
let ready = false;
loadUserCached(1, () => {
console.log('ready?', ready); // поведение зависит от ветки!
});
ready = true;
Анти-провал: если ты пишешь свой API, либо всегда вызывай callback асинхронно (например, через queueMicrotask(...)/setTimeout(..., 0)), либо явно фиксируй это в контракте.
Что такое callback простыми словами
Callback это функция, которую ты передаешь в другую функцию, чтобы она вызвала ее позже или при определенном событии. Это базовый механизм асинхронности в JavaScript, на котором долго строились API браузера и Node.js.
Ключевой момент: callback не выполняется в момент передачи. Он выполняется тогда, когда основная функция решит, что время пришло.
Проверь себя: чем "передать функцию" отличается от "вызвать функцию"?
Базовый синтаксис callback
function runTask(taskName, onDone) {
console.log(`Запущена задача: ${taskName}`);
onDone();
}
runTask('Импорт данных', () => {
console.log('Задача завершена');
});
Здесь onDone это callback. Он приходит как аргумент и вызывается внутри runTask.
Частая ошибка новичков: написать runTask('Импорт', onDone()). В этом случае функция вызывается сразу, а не передается как callback.
Callback в асинхронном сценарии
function loadConfig(onSuccess) {
setTimeout(() => {
onSuccess({ theme: 'dark' });
}, 300);
}
loadConfig((config) => {
console.log('Тема:', config.theme);
});
Результат (config) появляется позже, поэтому работа с ним идет внутри callback.
Смотри, что важно: callback помогает "привязать" код к моменту готовности данных.
Callback с ошибкой: error-first стиль
В Node.js распространен паттерн error-first callback: первым аргументом передается ошибка, вторым - успешный результат.
function readUser(id, callback) {
if (id <= 0) {
callback(new Error('Некорректный id'), null);
return;
}
callback(null, { id, name: 'Мария' });
}
readUser(1, (error, user) => {
if (error) {
console.log(error.message);
return;
}
console.log(user.name);
});
Это удобно, потому что обработка ошибки и успеха находится в одном месте вызова.
Проверь себя: почему после callback(error, null) важно делать return?
Смотри, что важно: return здесь защищает от "двойного ответа" — когда после ошибки код случайно доходит до ветки успеха и вызывает callback второй раз.
Проблема callback hell
Когда асинхронных шагов много и каждый вложен в предыдущий callback, код становится трудно читать и поддерживать.
loadUser((user) => {
loadOrders(user.id, (orders) => {
loadRecommendations(orders, (recs) => {
console.log(recs);
});
});
});
Такой "лесенкой" сложно управлять: растет вложенность, теряется обработка ошибок, сложнее дебажить.
Анти-провал:
- выноси шаги в именованные функции;
- держи единый формат обработки ошибок;
- не смешивай бизнес-логику и инфраструктурные детали в одном callback.
Микро-сценарии из реальной разработки
- Обработчик клика кнопки.
Ты передаешь callback в addEventListener, и браузер вызывает его, когда пользователь кликает.
- Результат валидации формы.
Функция проверки принимает callback и сообщает, что делать дальше: показать ошибку или отправить данные.
button.addEventListener('click', () => {
console.log('Кнопка нажата');
});
Что будет, если изменить входные данные
В readUser:
id = 1->error = null, естьuser;id = 0-> естьerror,user = null.
Это предсказуемая схема, и она особенно важна в ранних API.
Дополнительный пример: защита от двойного вызова callback.
function once(callback) {
let called = false;
return (...args) => {
if (called) return;
called = true;
callback(...args);
};
}
Частые ошибки новичков
- Вызывать callback несколько раз в одной ветке.
- Забывать обработать ошибку в
error-firstпаттерне. - Передавать
callback()вместоcallback. - Делать глубокую вложенность без разделения логики.
Проверь себя: что сломается, если callback вызовется дважды для одного запроса?
Краткий итог
Callbackэто функция, переданная как аргумент для отложенного вызова.- Он лежит в основе событий и многих асинхронных API.
- В Node.js часто используется
error-firstстиль. - Главный риск - глубокая вложенность (
callback hell) и слабая читаемость. - Четкий контракт callback и аккуратная обработка ошибок делают код надежнее.