Колбэки и ад колбэков

асинхронность≈ 7 минут

Здесь будет встроен видео-разбор темы (5–7 минут, YouTube / RuTube / VK Видео) — тем, кто лучше воспринимает на слух, текст ниже не обязателен.

Асинхронность в JavaScript начиналась с простого приёма: «вот тебе моя функция — позови её, когда закончишь». Приём честно работает ровно до третьего вложенного вызова, после которого код начинает уезжать вправо и получает прозвище ад колбэков.

Разделы темы:·

Функцию в JavaScript можно передать в другую функцию так же спокойно, как число или строку. Функцию, переданную ради того, чтобы её вызвали потом, называют колбэком (англ. call back — «перезвонить»). Вы не вызываете её сами: вы оставляете номер, по которому вас наберут.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Обратите внимание: скобок после callback при передаче нет. greet("Аня", fn) отдаёт саму функцию, greet("Аня", fn()) отдал бы её результат — самая частая опечатка новичка. И пока ничего асинхронного здесь нет: колбэк вызывается сразу, это просто ответ на вопрос «что сделать с результатом».

Настоящая польза появляется, когда результата пока не существует. Запрос к серверу, чтение файла, таймер — всё это заканчивается не в тот момент, когда вы написали строчку. Вернуть значение через return невозможно: возвращать нечего. Зато можно оставить функцию, которую вызовут, когда значение появится.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Порядок вывода важнее самого вывода: финиш печатается раньше, чем пришёл Аня. Движок не садится ждать таймер — он идёт дальше по коду, а колбэк вызывает позже, когда дойдут руки. Всё, что должно случиться «после», обязано жить внутри колбэка: снаружи данных ещё нет.

Колбэк — это не пауза. Вы не стоите у телефона: вы оставили номер и пошли дальше по коду.

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

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Это и есть ад колбэков (callback hell), он же «пирамида судьбы». Код рабочий, но читается он не сверху вниз, а вправо и вниз, а хвост из }); приходится расшифровывать глазами. Добавьте сюда шестой шаг и попробуйте вставить условие в середину — вот тут и становится больно.

Можно вынести каждый уровень в именованную функцию — отступы исчезнут.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Стало плоско, но читать теперь надо снизу вверх, и каждому шагу нужно имя. Это лечение симптома: вложенность ушла, а связанность шагов через чужие вызовы осталась.

У асинхронного колбэка нет try/catch: к моменту вызова функция, которая его приняла, давно завершилась, и try вокруг вызова уже ничего не поймает. Поэтому в Node.js прижилось соглашение error-first: первый аргумент колбэка — ошибка, второй — результат. Всё прошло хорошо — первым приходит null.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Соглашение простое, а дисциплина утомительная: проверку if (err) надо писать на каждом уровне вложенности. И имена начинают путаться — во внутреннем колбэке своя переменная err, которая перекрывает внешнюю, так что забытая проверка тихо теряет ошибку.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Главная беда колбэка — вовсе не отступы. Вы отдаёте свою функцию чужому коду и надеетесь: её вызовут, вызовут один раз, не забудут вызвать при ошибке, не позовут внезапно синхронно. Управление продолжением вашей программы оказалось у чужой библиотеки — это называют инверсией управления.

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

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
ВопросКолбэкПромис
Где живёт результату чужого кода, который зовёт вашу функциюв объекте, который можно передать и сохранить
Сколько раз сработаеткак решит библиотекаровно один раз
Ошибкапервым аргументом, проверка на каждом шагеодин .catch на всю цепочку
Три шага подрядлестница вправостолбик .then

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

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Ад рождается не от самого колбэка, а от цепочки асинхронных шагов, где каждый следующий ждёт предыдущего. Именно этот случай и забирают на себя промисы, а потом async/await — который внутри всё те же промисы, а под ними всё те же колбэки.

  • Колбэк — функция, переданная другой функции, чтобы её вызвали позже: сразу и синхронно (map, filter) или когда придёт результат (setTimeout, запрос).
  • Асинхронный колбэк не останавливает выполнение: строки после вызова отработают раньше самого колбэка, поэтому всё, что зависит от результата, живёт внутри него.
  • Три зависимых шага складываются в лестницу, растущую вправо, — это и есть ад колбэков: код читается не сверху вниз, а по диагонали.
  • Соглашение error-first ставит ошибку первым аргументом, а результат — вторым; проверку if (err) приходится повторять на каждом уровне вложенности.
  • Промисы решают не только отступы: результат становится значением, которое можно передать, а язык гарантирует однократный вызов и доставку ошибки в .catch.

1/3console.log("A"); setTimeout(() => console.log("B"), 0); console.log("C"); — что выведется?

2/3Колбэк в стиле error-first получил (null, data). Что это значит?

3/3Что даёт промис, чего не гарантирует обычный колбэк?

Переведите функцию на error-first

Функция делит числа и отдаёт результат в колбэк, но про деление на ноль ничего не знает — колбэк получает Infinity и считает это нормальным ответом.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Перепишите divide в стиле error-first: при b === 0 колбэк должен получить первым аргументом new Error("деление на ноль"), а в обычном случае — null и результат.

Ошибку отдаём первым аргументом и сразу выходим из функции — иначе колбэк вызовется дважды.

jsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

Приём с return callback(...) — стандартный: он и завершает функцию, и передаёт ошибку одной строкой.

Разверните лестницу в цепочку промисов

Три шага выполняются по очереди, и каждый вложен в предыдущий. Перепишите так, чтобы wait возвращала промис, а вызовы шли плоским столбиком .then — вывод должен остаться прежним.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Вместо аргумента done функция возвращает промис, а resolve зовём там же, где раньше звали колбэк.

jsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

Ключевая деталь: внутри .then промис нужно именно вернуть. Без return (здесь его заменяет короткая стрелка без фигурных скобок) цепочка не станет ждать следующий шаг и порядок сломается.