Промисы VS колбэки
На первый взгляд может показаться, что промисы — это просто синтаксический сахар над колбэками.
Ведь в then мы действительно передаём функцию, которая выполнится позже.
Однако между этими подходами есть идеологические и практические различия, и промисы решают целый класс проблем колбэков.
Callback-подход
readFile("data.json", (err, data) => {
if (err) {
// ...
} else {
// ...
}
});
У такого подхода есть ряд проблем. Самые важные:
-
Инверсия контроля
Мы передаём свою функцию чужому коду (readFile).
После этого мы теряем контроль:- когда её вызовут,
- сколько раз вызовут,
- вызовут ли вообще,
- поймают ли исключение внутри неё,
- не вызовут ли ошибочный и успешный колбэк одновременно.
-
Нет стандарта ошибок
Каждый API решает по-своему:- где-то
callback(err, data), - где-то два колбэка:
onSuccess,onError, - где-то ошибки вообще не передаются.
Это делает композицию разных API трудной.
- где-то
-
Сложно композиционировать
Когда нужно выполнить несколько операций параллельно или последовательно, появляется много ручной логики:- флаги,
- счётчики,
- проверка «все ли завершились»,
- ручная передача ошибок.
Код становится сложным.
Что привносят Promises
1. Promise — это контейнер будущего значения
const p = fetch("/api/user"); // p — это "обещание" получить ответ
Promise — это отдельное значение, которое:
- представляет одно конкретное асинхронное вычисление,
- может быть в трёх состояниях (
pending → fulfilled/rejected), - может быть сохранено, передано, возвращено из функции, помещено в массив и т.д.
Это уже не просто «функция, которую вызовут позже», а абстракция результата во времени.
2. Контракт и гарантии, которых нет у колбэков
Промисы обеспечивают то, что колбэки не гарантируют:
- Промис может выполниться или отклониться только один раз.
- После перехода в состояние промис immutable — состояние больше не меняется.
- Обработчики
.then/.catchвызываются:- всегда асинхронно, через microtask queue,
- в порядке их регистрации.
- Любая ошибка в обработчике автоматически превращается в
rejectedи пробрасывается дальше.
Это создаёт предсказуемую и безопасную модель работы.
3. Отделение происхождения результата от использования
С колбэком:
doSomething((result) => { /* ... */ });
Мы отдаём управление своему коду чужой функции.
С промисом:
const p = doSomething();
p.then((result) => { /* ... */ });
Это два разных этапа:
- функция создаёт промис и управляет состоянием,
- внешний код только подписывается на результат.
Это снижает инверсию контроля и делает код структурированнее.
4. Композиция «из коробки»
Промисы позволяют легко строить цепочки и композиции:
doA()
.then(resultA => doB(resultA))
.then(resultB => doC(resultB))
.catch(console.error);
И параллельные операции:
Promise.all([fetchUser(), fetchPosts()])
.then(([user, posts]) => { /* ... */ })
.catch(console.error);
С колбэками для этого пришлось бы писать счетчики, флаги и ручную обработку ошибок.
5. Сквозная обработка ошибок
doA()
.then(doB)
.then(doC)
.catch(err => {
// поймает ошибку из A, B или C
});
Любая ошибка в любом then автоматически проматывается вниз по цепочке, пока не встретит catch.
Не нужно писать обработчик ошибок в каждом шаге.
Итог: промисы — не сахар над колбэками
Технически промисы действительно работают поверх колбэков и очередей задач.
И в then мы передаём функцию — то есть формально это тоже колбэк.
Но по смыслу и возможностям промисы — другая модель асинхронности:
- с контрактами,
- с формальными гарантиями,
- с единым API ошибок,
- с удобной композицией,
- с предсказуемой очередью запуска,
- с переносимостью и универсальностью.