Справочник по JS

Промисы VS колбэки

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

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


Callback-подход

readFile("data.json", (err, data) => {
    if (err) {
        // ...
    } else {
        // ...
    }
});

У такого подхода есть ряд проблем. Самые важные:

  1. Инверсия контроля
    Мы передаём свою функцию чужому коду (readFile).
    После этого мы теряем контроль:

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

    • где-то callback(err, data),
    • где-то два колбэка: onSuccess, onError,
    • где-то ошибки вообще не передаются.

    Это делает композицию разных API трудной.

  3. Сложно композиционировать
    Когда нужно выполнить несколько операций параллельно или последовательно, появляется много ручной логики:

    • флаги,
    • счётчики,
    • проверка «все ли завершились»,
    • ручная передача ошибок.

    Код становится сложным.


Что привносят 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 ошибок,
  • с удобной композицией,
  • с предсказуемой очередью запуска,
  • с переносимостью и универсальностью.
23-11-2025
Сергей Железников

Другие материалы по теме