Колбэк — это функция, которую вы передаёте другой функции
В JavaScript функции — это обычные значения. Их можно сохранить в переменную, положить в массив и, что нам сейчас важнее всего, передать в качестве аргумента. Когда вы передаёте функцию в другую функцию, чтобы та вызвала её позже, переданная функция и называется колбэком (callback-функцией).
greet понятия не имеет, что именно делает formatter. Функция просто вызывает его с именем и использует результат. Какое поведение получить — решаешь ты, передавая разные колбэк-функции. Ради этой гибкости колбэки и нужны.
Синхронные колбэки выполняются сразу
Не каждый callback в JavaScript — асинхронный. Многие методы массивов, которыми ты уже пользуешься, построены на колбэках и вызывают их синхронно — ещё до того, как внешний вызов завершится:
map, filter и reduce принимают колбэк-функцию и сразу же вызывают её для каждого элемента. К моменту, когда map вернёт результат, все вызовы колбэка уже отработали. Ничего не откладывается на потом.
Это классический паттерн функции высшего порядка — «вот данные, вот что с ними сделать, верни мне результат». Event loop тут ни при чём.
Асинхронный callback в JavaScript выполняется позже
Когда говорят «callback функция», обычно имеют в виду именно асинхронный вариант. Вы передаёте функцию в какой-нибудь API, которому нужно время на работу — таймер, сетевой запрос, чтение файла — и этот API вызывает вашу функцию, когда задача завершится.
Порядок вывода будет такой: до, после, а через секунду — таймер сработал. setTimeout не ставит программу на паузу. Он передаёт колбэк рантайму, сразу возвращает управление, и остальной скрипт продолжает работать. Через секунду event loop подхватывает колбэк и выполняет его.
Схема «вернуться сейчас, перезвонить потом» — это и есть ментальная модель любого асинхронного callback в JavaScript: от addEventListener до старых файловых API в Node.js.
Соглашение error-first (Node.js)
До появления промисов в Node.js устоялась конкретная форма колбэка: первый аргумент — это ошибка (или null), а остальные — собственно результат. В старом коде и некоторых библиотеках вы с ней ещё столкнётесь.
Вызывающий код сначала проверяет err и сразу выходит, если там что-то есть. И только потом уже работает с результатом. Это соглашение — язык его никак не навязывает, — но стоит один раз увидеть сигнатуру (err, result) => ..., и вы будете узнавать её повсюду.
Callback hell: ад вложенных колбэков
Проблемы начинаются, когда один асинхронный шаг зависит от результата другого. Каждый колбэк приходится класть внутрь предыдущего, и код уезжает лесенкой всё дальше и дальше:
Это та самая печально известная «пирамида дума», она же callback hell. Боль тут вполне конкретная:
- Поток выполнения идёт зигзагом, а не читается сверху вниз.
- На каждом уровне приходится копипастить один и тот же шаблон
if (err) return .... - Если внутренний колбэк бросит исключение, наружу оно не всплывёт — ошибки нужно ловить на каждом слое отдельно.
- Любой рефакторинг превращается в переотступление всего блока.
Частично спасает вынос именованных функций, но корень проблемы никуда не девается: на голых колбэках асинхронный код просто неудобно собирать. Именно ради этого и придумали промисы.
Пара подводных камней, о которых стоит помнить
Не вызывайте колбэк случайно. Когда вы передаёте колбэк, вы передаёте саму функцию — а не результат её вызова.
Осторожно с this. Если ваш колбэк — это обычная функция, внутри которой используется this, то значение this будет зависеть от того, как колбэк вызывают, а не от того, где он был объявлен. Стрелочные функции обходят эту проблему стороной — они берут this из окружающего контекста:
Стрелочные функции — это дефолтный выбор для инлайновых колбэков именно по этой причине.
Callback vs Promise: что выбрать
Колбэк-функции по-прежнему живут в синхронных API (map, forEach, sort), в обработчиках событий (element.addEventListener("click", ...)) и в низкоуровневых хуках рантайма. Но для асинхронных задач, которые возвращают один результат, экосистема почти полностью перешла на промисы.
Коротко о разнице:
- Колбэки — просто и минималистично, но плохо компонуются. Ошибки приходится обрабатывать вручную на каждом шаге.
- Промисы — объект, который представляет будущий результат. Их можно сцеплять через
.then(), ошибки ловить одним.catch()— и вся «пирамида» выпрямляется.
Разбираться в колбэках всё равно придётся: на них построены сами промисы, да и в event-driven коде они повсюду. Просто новые асинхронные API на голых колбэках уже почти никто не пишет.
Дальше: промисы
Промисы берут идею «сделай это, когда будет готово то» и оборачивают её в объект, который можно передавать, сцеплять и комбинировать. Об этом — следующая страница. А оттуда уже рукой подать до async/await — того, как современный JavaScript в основном и работает с асинхронностью.
Часто задаваемые вопросы
Что такое callback-функция в JavaScript?
Callback — это функция, которую вы передаёте в другую функцию как аргумент, чтобы та вызвала её позже. Например, setTimeout(() => console.log('hi'), 1000) передаёт стрелочную функцию в качестве колбэка: setTimeout запоминает её и вызывает, когда срабатывает таймер. Исторически именно через колбэки в JavaScript решали задачу «сделай это, когда будет готово вон то».
Чем синхронный callback отличается от асинхронного?
Синхронный колбэк выполняется сразу, внутри того же вызова, которому его передали. Скажем, [1, 2, 3].map(x => x * 2) трижды вызовет функцию ещё до того, как map вернёт результат. Асинхронный же сохраняется и вызывается позже, когда произойдёт какое-то событие — так работают setTimeout, fs.readFile и обработчики DOM-событий. Асинхронные колбэки не блокируют остальной код.
Что такое callback hell и как его избежать?
Callback hell — это та самая «пирамида вправо», которая получается, когда асинхронные колбэки зависят друг от друга и вкладываются один в другой на несколько уровней. За потоком управления и обработкой ошибок в таком коде следить невозможно. Лечится это промисами с цепочками .then(), а ещё лучше — async/await: оба подхода распрямляют пирамиду во вменяемый линейный код.