Menu
Русский

Callback в JavaScript: функции-аргументы и callback hell

Разбираемся, как работают callback-функции в JavaScript: передача функций как аргументов, паттерн error-first и почему вложенные колбэки довели разработчиков до промисов.

Колбэк — это функция, которую вы передаёте другой функции

В JavaScript функции — это обычные значения. Их можно сохранить в переменную, положить в массив и, что нам сейчас важнее всего, передать в качестве аргумента. Когда вы передаёте функцию в другую функцию, чтобы та вызвала её позже, переданная функция и называется колбэком (callback-функцией).

index.js
Output
Click Run to see the output here.

greet понятия не имеет, что именно делает formatter. Функция просто вызывает его с именем и использует результат. Какое поведение получить — решаешь ты, передавая разные колбэк-функции. Ради этой гибкости колбэки и нужны.

Синхронные колбэки выполняются сразу

Не каждый callback в JavaScript — асинхронный. Многие методы массивов, которыми ты уже пользуешься, построены на колбэках и вызывают их синхронно — ещё до того, как внешний вызов завершится:

index.js
Output
Click Run to see the output here.

map, filter и reduce принимают колбэк-функцию и сразу же вызывают её для каждого элемента. К моменту, когда map вернёт результат, все вызовы колбэка уже отработали. Ничего не откладывается на потом.

Это классический паттерн функции высшего порядка — «вот данные, вот что с ними сделать, верни мне результат». Event loop тут ни при чём.

Асинхронный callback в JavaScript выполняется позже

Когда говорят «callback функция», обычно имеют в виду именно асинхронный вариант. Вы передаёте функцию в какой-нибудь API, которому нужно время на работу — таймер, сетевой запрос, чтение файла — и этот API вызывает вашу функцию, когда задача завершится.

index.js
Output
Click Run to see the output here.

Порядок вывода будет такой: до, после, а через секунду — таймер сработал. setTimeout не ставит программу на паузу. Он передаёт колбэк рантайму, сразу возвращает управление, и остальной скрипт продолжает работать. Через секунду event loop подхватывает колбэк и выполняет его.

Схема «вернуться сейчас, перезвонить потом» — это и есть ментальная модель любого асинхронного callback в JavaScript: от addEventListener до старых файловых API в Node.js.

Соглашение error-first (Node.js)

До появления промисов в Node.js устоялась конкретная форма колбэка: первый аргумент — это ошибка (или null), а остальные — собственно результат. В старом коде и некоторых библиотеках вы с ней ещё столкнётесь.

index.js
Output
Click Run to see the output here.

Вызывающий код сначала проверяет err и сразу выходит, если там что-то есть. И только потом уже работает с результатом. Это соглашение — язык его никак не навязывает, — но стоит один раз увидеть сигнатуру (err, result) => ..., и вы будете узнавать её повсюду.

Callback hell: ад вложенных колбэков

Проблемы начинаются, когда один асинхронный шаг зависит от результата другого. Каждый колбэк приходится класть внутрь предыдущего, и код уезжает лесенкой всё дальше и дальше:

index.js
Output
Click Run to see the output here.

Это та самая печально известная «пирамида дума», она же callback hell. Боль тут вполне конкретная:

  • Поток выполнения идёт зигзагом, а не читается сверху вниз.
  • На каждом уровне приходится копипастить один и тот же шаблон if (err) return ....
  • Если внутренний колбэк бросит исключение, наружу оно не всплывёт — ошибки нужно ловить на каждом слое отдельно.
  • Любой рефакторинг превращается в переотступление всего блока.

Частично спасает вынос именованных функций, но корень проблемы никуда не девается: на голых колбэках асинхронный код просто неудобно собирать. Именно ради этого и придумали промисы.

Пара подводных камней, о которых стоит помнить

Не вызывайте колбэк случайно. Когда вы передаёте колбэк, вы передаёте саму функцию — а не результат её вызова.

index.js
Output
Click Run to see the output here.

Осторожно с this. Если ваш колбэк — это обычная функция, внутри которой используется this, то значение this будет зависеть от того, как колбэк вызывают, а не от того, где он был объявлен. Стрелочные функции обходят эту проблему стороной — они берут this из окружающего контекста:

index.js
Output
Click Run to see the output here.

Стрелочные функции — это дефолтный выбор для инлайновых колбэков именно по этой причине.

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: оба подхода распрямляют пирамиду во вменяемый линейный код.

Учитесь программировать с Coddy

НАЧАТЬ