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.

Микро-сценарии из реальной разработки

  1. Обработчик клика кнопки.

Ты передаешь callback в addEventListener, и браузер вызывает его, когда пользователь кликает.

  1. Результат валидации формы.

Функция проверки принимает 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 и аккуратная обработка ошибок делают код надежнее.