Learning Book

Promises

Проблема: callback hell

До промисов асинхронный код строился на колбэках. При нескольких последовательных операциях код превращался в «пирамиду смерти»:

// ❌ Callback hell
getUser(userId, function(err, user) {
  if (err) return handleError(err)
  getPosts(user.id, function(err, posts) {
    if (err) return handleError(err)
    getComments(posts[0].id, function(err, comments) {
      if (err) return handleError(err)
      render(user, posts, comments)
    })
  })
})

Проблемы: глубокая вложенность, дублирование обработки ошибок, невозможность использовать try/catch.

Что такое Promise

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

Три состояния:

  • pending — ожидание (начальное)
  • fulfilled — успешно завершён (есть значение)
  • rejected — завершён с ошибкой (есть причина)
const promise = new Promise((resolve, reject) => {
  // Асинхронная операция
  setTimeout(() => {
    resolve('Готово!') // → fulfilled, значение: 'Готово!'
    // или: reject(new Error('Упс')) // → rejected
  }, 1000)
})

Состояние промиса необратимо: из pending он переходит в fulfilled или rejected один раз. Повторные вызовы resolve/reject игнорируются.

Цепочки: .then(), .catch(), .finally()

Каждый .then() возвращает новый промис, позволяя строить плоские цепочки:

fetch('/api/users/1')
  .then(response => response.json())    // → новый промис с JSON
  .then(user => fetch(`/api/posts?userId=${user.id}`))
  .then(response => response.json())    // → новый промис с постами
  .then(posts => renderPosts(posts))
  .catch(err => showError(err))          // ловит ошибку из любого шага
  .finally(() => hideSpinner())          // выполняется всегда

Критически важно: всегда возвращай значение из .then(). Без return следующий .then() получит undefined:

// ❌ Забыли return — floating promise
fetch(url).then(r => {
  r.json() // промис создан, но не возвращён
})

// ✅ Правильно
fetch(url).then(r => r.json())

Обработка ошибок

.catch() ловит ошибки из любого предыдущего .then():

Promise.resolve(1)
  .then(v => { throw new Error('Бум!') })
  .then(v => console.log('Не выполнится'))
  .catch(err => {
    console.log(err.message) // 'Бум!'
    return 'Восстановились'  // цепочка продолжается!
  })
  .then(v => console.log(v)) // 'Восстановились'

После .catch() цепочка продолжается — это позволяет реализовать паттерн восстановления.

Unhandled rejection

Если промис отклонён и нигде нет .catch(), возникает событие unhandledrejection:

// ❌ Нет обработки — unhandled rejection
async function loadData() {
  const data = await fetch('/broken-url') // упадёт
}
loadData() // Кто ловит ошибку? Никто!

// ✅ Всегда обрабатывай ошибки
loadData().catch(err => console.error(err))

Promise API: статические методы

Promise.all — все или ничего

Ждёт выполнения всех промисов. Если хотя бы один отклоняется — весь Promise.all отклоняется:

const [user, posts, config] = await Promise.all([
  fetch('/api/user').then(r => r.json()),
  fetch('/api/posts').then(r => r.json()),
  fetch('/api/config').then(r => r.json()),
])
// Все три запроса запускаются одновременно, результат — когда все готовы

Promise.allSettled — подождать всех

Ждёт все промисы, не прерываясь при ошибках. Возвращает массив объектов с status:

const results = await Promise.allSettled([
  fetch('/api/users'),
  fetch('/api/broken'),  // упадёт
  fetch('/api/posts'),
])

results.forEach(r => {
  if (r.status === 'fulfilled') console.log('OK:', r.value)
  if (r.status === 'rejected') console.log('Error:', r.reason)
})

Promise.race — первый финиширует

Возвращает результат первого settled промиса (fulfilled или rejected):

// Таймаут для fetch
const result = await Promise.race([
  fetch('/api/data'),
  new Promise((_, reject) =>
    setTimeout(() => reject(new Error('Timeout')), 5000)
  ),
])

Promise.any — первый успешный

Возвращает результат первого fulfilled промиса. Отклоняется только если все отклонены (AggregateError):

// Пробуем несколько зеркал, берём первый ответивший
const data = await Promise.any([
  fetch('https://mirror1.example.com/data'),
  fetch('https://mirror2.example.com/data'),
  fetch('https://mirror3.example.com/data'),
])

Сравнительная таблица

МетодЖдётРеджектит при
Promise.allВсе fulfilledПервом reject
Promise.allSettledВсе settledНикогда
Promise.raceПервый settledЕсли первый — reject
Promise.anyПервый fulfilledВсе rejected

Антипаттерн: Promise constructor

Не оборачивай промисы в new Promise — это избыточно и может проглатывать ошибки:

// ❌ Promise constructor antipattern
function getData(url) {
  return new Promise((resolve, reject) => {
    fetch(url)
      .then(r => r.json())
      .then(data => resolve(data))
      .catch(err => reject(err))
  })
}

// ✅ fetch уже возвращает Promise
function getData(url) {
  return fetch(url).then(r => r.json())
}

new Promise нужен только для оборачивания callback-based API (например, setTimeout, fs.readFile без промис-версии).

Колбэки .then() — всегда асинхронны

Даже для уже resolved промиса, .then() выполняется асинхронно (как микрозадача):

console.log('1')
Promise.resolve('2').then(v => console.log(v))
console.log('3')

// Вывод: 1, 3, 2 — .then() всегда откладывается

Это гарантирует предсказуемый порядок выполнения вне зависимости от того, resolved промис или нет.

Итого

КонцепцияОписание
PromiseОбъект-обёртка для асинхронного результата
Состоянияpending → fulfilled / rejected (необратимо)
Цепочки.then() возвращает новый промис
Ошибки.catch() ловит ошибки из всей цепочки
allВсе успешны или reject при первой ошибке
allSettledЖдёт всех, возвращает статусы
raceПервый settled (любой)
anyПервый fulfilled