Колбэки и ад колбэков
Здесь будет встроен видео-разбор темы (5–7 минут, YouTube / RuTube / VK Видео) — тем, кто лучше воспринимает на слух, текст ниже не обязателен.
Асинхронность в JavaScript начиналась с простого приёма: «вот тебе моя функция — позови её, когда закончишь». Приём честно работает ровно до третьего вложенного вызова, после которого код начинает уезжать вправо и получает прозвище ад колбэков.
Функцию в JavaScript можно передать в другую функцию так же спокойно, как число или строку. Функцию, переданную ради того, чтобы её вызвали потом, называют колбэком (англ. call back — «перезвонить»). Вы не вызываете её сами: вы оставляете номер, по которому вас наберут.
Обратите внимание: скобок после callback при передаче нет. greet("Аня", fn) отдаёт саму функцию, greet("Аня", fn()) отдал бы её результат — самая частая опечатка новичка. И пока ничего асинхронного здесь нет: колбэк вызывается сразу, это просто ответ на вопрос «что сделать с результатом».
Настоящая польза появляется, когда результата пока не существует. Запрос к серверу, чтение файла, таймер — всё это заканчивается не в тот момент, когда вы написали строчку. Вернуть значение через return невозможно: возвращать нечего. Зато можно оставить функцию, которую вызовут, когда значение появится.
Порядок вывода важнее самого вывода: финиш печатается раньше, чем пришёл Аня. Движок не садится ждать таймер — он идёт дальше по коду, а колбэк вызывает позже, когда дойдут руки. Всё, что должно случиться «после», обязано жить внутри колбэка: снаружи данных ещё нет.
Колбэк — это не пауза. Вы не стоите у телефона: вы оставили номер и пошли дальше по коду.
Пока шаг один, всё честно. Беда начинается, когда шагов три и каждый следующий зависит от предыдущего: сначала пользователь, потом его заказы, потом детали заказа. Единственный способ дождаться — вложить колбэк в колбэк, а его — ещё в один.
Это и есть ад колбэков (callback hell), он же «пирамида судьбы». Код рабочий, но читается он не сверху вниз, а вправо и вниз, а хвост из }); приходится расшифровывать глазами. Добавьте сюда шестой шаг и попробуйте вставить условие в середину — вот тут и становится больно.
Можно вынести каждый уровень в именованную функцию — отступы исчезнут.
Стало плоско, но читать теперь надо снизу вверх, и каждому шагу нужно имя. Это лечение симптома: вложенность ушла, а связанность шагов через чужие вызовы осталась.
У асинхронного колбэка нет try/catch: к моменту вызова функция, которая его приняла, давно завершилась, и try вокруг вызова уже ничего не поймает. Поэтому в Node.js прижилось соглашение error-first: первый аргумент колбэка — ошибка, второй — результат. Всё прошло хорошо — первым приходит null.
Соглашение простое, а дисциплина утомительная: проверку if (err) надо писать на каждом уровне вложенности. И имена начинают путаться — во внутреннем колбэке своя переменная err, которая перекрывает внешнюю, так что забытая проверка тихо теряет ошибку.
Главная беда колбэка — вовсе не отступы. Вы отдаёте свою функцию чужому коду и надеетесь: её вызовут, вызовут один раз, не забудут вызвать при ошибке, не позовут внезапно синхронно. Управление продолжением вашей программы оказалось у чужой библиотеки — это называют инверсией управления.
Промис переворачивает картину. Функция больше не забирает вашу функцию — она возвращает объект, обещание будущего результата. Обещание можно положить в переменную, передать дальше, навесить обработчик хоть через секунду. А гарантии даёт сам язык: сработает ровно один раз, ошибка не потеряется, вызов всегда асинхронный.
| Вопрос | Колбэк | Промис |
|---|---|---|
| Где живёт результат | у чужого кода, который зовёт вашу функцию | в объекте, который можно передать и сохранить |
| Сколько раз сработает | как решит библиотека | ровно один раз |
| Ошибка | первым аргументом, проверка на каждом шаге | один .catch на всю цепочку |
| Три шага подряд | лестница вправо | столбик .then |
Колбэки никуда не делись и не считаются злом. map, filter, sort, обработчики событий — все они принимают функцию, и это нормальный, читаемый код.
Ад рождается не от самого колбэка, а от цепочки асинхронных шагов, где каждый следующий ждёт предыдущего. Именно этот случай и забирают на себя промисы, а потом 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 и считает это нормальным ответом.
Перепишите divide в стиле error-first: при b === 0 колбэк должен получить первым аргументом new Error("деление на ноль"), а в обычном случае — null и результат.
Ошибку отдаём первым аргументом и сразу выходим из функции — иначе колбэк вызовется дважды.
Приём с return callback(...) — стандартный: он и завершает функцию, и передаёт ошибку одной строкой.
Разверните лестницу в цепочку промисов
Три шага выполняются по очереди, и каждый вложен в предыдущий. Перепишите так, чтобы wait возвращала промис, а вызовы шли плоским столбиком .then — вывод должен остаться прежним.
Вместо аргумента done функция возвращает промис, а resolve зовём там же, где раньше звали колбэк.
Ключевая деталь: внутри .then промис нужно именно вернуть. Без return (здесь его заменяет короткая стрелка без фигурных скобок) цепочка не станет ждать следующий шаг и порядок сломается.