Цепочки промисов и ошибки

Цепочки промисов и ошибки #

Цепочка промисов — последовательность вызовов .then (и при необходимости .catch / .finally), где каждый шаг получает результат предыдущего. Ошибка в любом звене «скатывается» вниз до ближайшего .catch, если её не обработали.


Линейный сценарий оформления заказа #

function validateCart(cart) {
  if (!cart.items?.length) throw new Error('Пустая корзина')
  return cart
}

function reserveItems(cart) {
  return Promise.resolve({ ...cart, reserved: true })
}

function chargePayment(cart) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (cart.total > 50_000) reject(new Error('Нужно подтверждение банка'))
      else resolve({ ...cart, paid: true })
    }, 200)
  })
}

function sendReceipt(order) {
  return Promise.resolve({ ...order, receiptSent: true })
}

const cart = { items: [{ sku: 'bag-01' }], total: 1200 }

Promise.resolve(cart)
  .then(validateCart)
  .then(reserveItems)
  .then(chargePayment)
  .then(sendReceipt)
  .then((order) => console.log('Готово:', order.receiptSent))
  .catch((err) => console.error('Оформление сорвалось:', err.message))

Каждый .then возвращает новое значение или промис — цепочка остаётся плоской, без вложенности колбэков.


Как распространяется ошибка #

  1. Синхронный throw внутри .then превращает звено в rejected.
  2. Возврат Promise.reject(...) из .then — то же самое.
  3. Необработанный сбой в исполнителе new Promise отклоняет промис.
Promise.resolve()
  .then(() => {
    throw new Error('Шаг 2')
  })
  .then(() => {
    console.log('не выполнится')
  })
  .catch((err) => {
    console.log(err.message) // "Шаг 2"
    return 'восстановлено'
  })
  .then((value) => {
    console.log(value) // "восстановлено"
  })

После .catch, который вернул обычное значение, цепочка снова в состоянии fulfilled.


catch в середине цепочки #

Локальный .catch обрабатывает ошибку и может либо завершить сценарий, либо продолжить:

chargePayment(cart)
  .catch((err) => {
    if (err.message.includes('банка')) {
      return { ...cart, paid: false, pendingBank: true }
    }
    throw err // другие ошибки — дальше по цепочке
  })
  .then((state) => {
    if (state.pendingBank) {
      console.log('Ожидаем 3-D Secure')
    }
  })

Различайте восстановление (вернули значение) и повторный проброс (throw err).


Вложенные then — антипаттерн #

Плохо — дублируется структура колбэков:

loadCart().then((cart) => {
  return chargePayment(cart).then((paid) => {
    return sendReceipt(paid).then((order) => order)
  })
})

Лучше — последовательные .then или async/await:

loadCart()
  .then(chargePayment)
  .then(sendReceipt)

Если шаги независимы и могут идти параллельно, используйте Promise.all (отдельная статья), а не цепочку.


finally не меняет ветку #

chargePayment(cart)
  .then((result) => result)
  .finally(() => {
    console.log('Снятие индикатора загрузки')
  })
  .then((result) => {
    // result — то же, что вернул chargePayment (если не было throw в finally)
  })

finally не принимает аргументов результата; если внутри finally бросить ошибку, последующие .then не получат исходное значение.


Необработанное отклонение #

Промис без .catch в конце цепочки при отклонении может привести к событию unhandledrejection (браузер / Node). В продакшене всегда завершайте пользовательские цепочки обработчиком или логированием.

doCheckout()
  .then(showSuccess)
  .catch((err) => {
    showError(err.message)
    reportToMonitoring(err)
  })

Итог #

  • Цепочка — плоская композиция шагов через возврат значений и промисов из .then.
  • Ошибка прерывает промежуточные .then и попадает в ближайший .catch.
  • Локальный .catch может восстановить сценарий или пробросить ошибку дальше.
  • Избегайте вложенных .then; для параллели — Promise.all, для линейной логики — цепочка или async/await.

См. также #

Часто спрашивают

Что выбрать — then или async/await?
async/await обычно читается проще для последовательного кода; then удобен для коротких цепочек и комбинирования с Promise API. Подробнее
Чем Promise.all отличается от allSettled?
all падает при первой ошибке; allSettled дожидается всех и отдаёт статус каждого результата. Подробнее