Промисы: основы

Промисы: основы #

Промис (Promise) — объект, представляющий результат асинхронной операции, который может быть доступен сейчас, позже или никогда. Вместо вложенных колбэков вы получаете значение (или ошибку) через методы .then, .catch и .finally.


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

СостояниеЗначение
pendingоперация ещё не завершена
fulfilledуспех, есть результат
rejectedсбой, есть причина (обычно Error)

Переход из pending однократный: либо fulfilled, либо rejected, без возврата назад.

const pending = new Promise(() => {})
// pending остаётся pending, пока не вызовут resolve/reject

new Promise #

Низкоуровневое создание — функция-исполнитель с resolve и reject:

function requestInvoice(orderId) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (!orderId.startsWith('ord-')) {
        reject(new Error('Неверный формат orderId'))
        return
      }
      resolve({ orderId, total: 3290, currency: 'RUB' })
    }, 400)
  })
}

requestInvoice('ord-8841').then((invoice) => {
  console.log(invoice.total) // 3290
})

Исполнитель запускается сразу при создании промиса. Тяжёлую синхронную работу внутри исполнителя избегайте — блокируете поток.


then, catch, finally #

requestInvoice('bad-id')
  .then((invoice) => {
    return invoice.total
  })
  .catch((err) => {
    console.error(err.message) // "Неверный формат orderId"
    return 0
  })
  .finally(() => {
    console.log('Запрос счёта завершён')
  })
  • .then(onFulfilled, onRejected?) — обработка успеха; необязательный второй аргумент — устаревший стиль, лучше .catch.
  • .catch(onRejected) — сокращение для .then(null, onRejected).
  • .finally(onFinally) — код после завершения, без изменения переданного значения (кроме явного throw).

.then / .catch / .finally возвращают новый промис — основа цепочек (следующая статья).


Возврат значения и вложенный промис #

function loadCart() {
  return Promise.resolve({ items: [{ sku: 'pen-01', qty: 2 }] })
}

loadCart()
  .then((cart) => {
    const count = cart.items.reduce((n, i) => n + i.qty, 0)
    return count // оборачивается в Promise.resolve(count)
  })
  .then((count) => {
    console.log(count) // 2
  })

Если из .then вернуть другой промис, следующий .then дождётся его разрешения:

loadCart()
  .then((cart) => requestInvoice(cart.orderId ?? 'ord-0000'))
  .then((invoice) => console.log(invoice.total))

Promise.resolve и Promise.reject #

Готовые промисы для старта цепочки или тестов:

Promise.resolve(42).then((n) => n * 2) // 84

Promise.reject(new Error('Отмена оплаты')).catch((err) => {
  console.log(err.message)
})

Синхронная функция, которая может бросить исключение, удобно оборачивается так:

function safeParse(json) {
  return Promise.resolve().then(() => JSON.parse(json))
}

safeParse('{"ok":true}').then(console.log)
safeParse('broken').catch((err) => console.error(err.name)) // SyntaxError

От колбэка к промису #

Обёртка «промисификация» инкапсулирует соглашение (err, data):

function fetchTrackMetaAsync(trackId) {
  return new Promise((resolve, reject) => {
    fetchTrackMeta(trackId, (err, meta) => {
      if (err) reject(err)
      else resolve(meta)
    })
  })
}

// использование
fetchTrackMetaAsync('tr-01').then((meta) => console.log(meta.title))

Современные API ( fetch, fs.promises в Node) уже возвращают промисы — обёртки нужны для legacy-кода.


Итог #

  • Промис фиксирует исход async-операции в одном из трёх состояний.
  • .then / .catch / .finally подписываются на результат и строят цепочки.
  • Возврат значения или промиса из .then задаёт вход для следующего звена.
  • Promise.resolve / reject и обёртки над колбэками упрощают миграцию старого кода.

См. также #

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

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