JavaScriptMiddleTechnical

Что такое Promise? Чем они отличаются от callback'ов?

Promise — объект для асинхронного результата с тремя состояниями (pending/fulfilled/rejected). В отличие от callback, даёт плоские цепочки .then(), единый .catch() и удобную композицию через Promise.all/race/any.

Promise в JavaScript

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

  • pending — операция выполняется
  • fulfilled — операция завершилась успешно, есть значение
  • rejected — операция завершилась с ошибкой, есть причина

Переход из pending в fulfilled или rejected необратим.

Callback: исходная модель

До появления Promise (ES2015) асинхронность строилась на колбэках — функциях, передаваемых в качестве аргументов и вызываемых по завершении операции. Узнаваемый паттерн Node.js — error-first callback:

fs.readFile('data.json', 'utf8', (err, data) => {
  if (err) {
    console.error('Ошибка:', err);
    return;
  }
  JSON.parse(data, (parseErr, obj) => { // callback hell начинается здесь
    if (parseErr) {
      console.error('JSON error:', parseErr);
      return;
    }
    saveToDb(obj, (dbErr, result) => {
      if (dbErr) { /* ещё уровень */ }
      // ...
    });
  });
});

Promise: решение проблем callback

// Тот же поток с промисами
fs.promises.readFile('data.json', 'utf8')
  .then(data => JSON.parse(data))
  .then(obj => saveToDb(obj))
  .then(result => console.log('Saved:', result))
  .catch(err => console.error('Ошибка:', err)); // одна точка обработки ошибок

async/await — синтаксический сахар над Promise

async function processFile() {
  try {
    const data = await fs.promises.readFile('data.json', 'utf8');
    const obj = JSON.parse(data);
    const result = await saveToDb(obj);
    console.log('Saved:', result);
  } catch (err) {
    console.error('Ошибка:', err);
  }
}

Создание Promise вручную

function delay(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

function fetchWithRetry(url, retries = 3) {
  return new Promise(async (resolve, reject) => {
    for (let i = 0; i < retries; i++) {
      try {
        const res = await fetch(url);
        if (!res.ok) throw new Error(`HTTP ${res.status}`);
        return resolve(await res.json());
      } catch (err) {
        if (i === retries - 1) reject(err);
        else await delay(1000 * 2 ** i); // экспоненциальный backoff
      }
    }
  });
}

Ключевые отличия от callback

  • Единая обработка ошибок — один .catch() покрывает всю цепочку, вместо проверки if (err) на каждом уровне.
  • Компонуемость — Promise.all, race, any, allSettled для параллельного выполнения; callback-и требуют ручного счётчика.
  • Нет «callback hell» — цепочка .then() линейна; глубокая вложенность заменяется плоским кодом.
  • Гарантированный async — обработчик .then() всегда вызывается асинхронно (как микрозадача), даже если промис уже fulfilled; callback может быть вызван синхронно.
  • Состояние хранится — к fulfilled промису можно подключить .then() в любой момент и получить значение; callback вызывается ровно один раз и значение теряется.

Promisification — обёртка над callback API

const { promisify } = require('util');
const readFile = promisify(require('fs').readFile);

// Теперь readFile возвращает Promise
const content = await readFile('file.txt', 'utf8');

// Вручную
function promisifyCallback(fn) {
  return (...args) => new Promise((resolve, reject) => {
    fn(...args, (err, result) => {
      if (err) reject(err);
      else resolve(result);
    });
  });
}

Подводные камни

  • Проглоченные ошибки — Promise без .catch() тихо поглощает исключения; в Node.js ≥15 это завершает процесс с unhandledRejection.
  • new Promise(async executor) — async-функция в конструкторе Promise опасна: брошенное исключение внутри не попадёт в reject промиса, а уйдёт в unhandledRejection.
  • return в .then() — забытый return прерывает цепочку: следующий .then() получит undefined вместо нужного значения.
  • Смешивание async/await и .then() — технически корректно, но создаёт путаницу; выбирайте один стиль в рамках функции.
  • Promise не отменяемый — нет встроенной отмены; используйте AbortController + AbortSignal для fetch и других отменяемых операций.
  • await в циклеfor...of с await выполняет итерации последовательно; для параллельного выполнения нужен Promise.all(items.map(...)).
  • Ошибки после resolve/reject — если в executor выбросить исключение после вызова resolve, оно проигнорируется; Promise уже settled.

Common mistakes

  • Смешивать «Promise» с похожим механизмом без критерия выбора.
  • Игнорировать риск: неверно оценить границы применения темы «Promise» и получить хрупкое решение.
  • Показывать только синтаксис и не объяснять поведение в runtime или сборке.

What the interviewer is testing

  • Объясняет композиция асинхронного результата и отличие от callback-цепочек.
  • Показывает на примере, как работает: Promise представляет будущий результат в состояниях pending, fulfilled или rejected и позволяет строить цепочки с единым каналом ошибок вместо ручной передачи callback-функций.
  • Называет production-нюанс и граничный случай для темы «Promise».

Sources

Related topics

Что такое Promise? Чем они отличаются от callback'ов? | Talanto