Клавиша / esc
Две руки держатся за мизинчики, давая обещание
Иллюстрация: Кира Кустова

Promise

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

Время чтения: 11 мин

Кратко

Скопировано

Промис (Promise) — специальный объект JavaScript, который используется для написания и обработки асинхронного кода.

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

Промис может находиться в одном из трёх состояний:

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

Поменять состояние можно только один раз: перейти из pending либо в fulfilled, либо в rejected:

Схема трёх состояний промиса и переход между ними

У промиса есть методы then() и catch(), которые позволяют использовать результат работы промиса, и метод finally() для выполнения кода после получения результата вне зависимости от того, каков он.

Как пишется

Скопировано

Промис создаётся с помощью конструктора.

В конструктор передаётся функция-исполнитель асинхронной операции (англ. executor). Она вызывается сразу после создания промиса. Задача этой функции — выполнить асинхронную операцию и перевести состояние промиса в fulfilled (успех) или rejected (ошибка).

Изменить состояние промиса можно, вызвав колбэки, переданные аргументами в функцию:

2 колбэка Promise функции
        
          
          const promise = new Promise(function (resolve, reject) {  // Делаем асинхронную операцию: запрос в БД, API, etc.  const data = getData()  // Переводим промис в состояние fulfilled.  // Результат выполнения — объект data  resolve(data)})const errorPromise = new Promise(function (resolve, reject) {  // Переводим промис в состояние rejected.  // Результат выполнения — объект Error  reject(new Error('ошибка'))})
          const promise = new Promise(function (resolve, reject) {
  // Делаем асинхронную операцию: запрос в БД, API, etc.
  const data = getData()
  // Переводим промис в состояние fulfilled.
  // Результат выполнения — объект data
  resolve(data)
})

const errorPromise = new Promise(function (resolve, reject) {
  // Переводим промис в состояние rejected.
  // Результат выполнения — объект Error
  reject(new Error('ошибка'))
})

        
        
          
        
      

Первый параметр (в примере кода назван resolve) — колбэк для перевода промиса в состояние fulfilled, при его вызове аргументом передаётся результат операции.

Второй параметр (в примере кода назван reject) — колбэк для перевода промиса в состояние rejected, при его вызове аргументом передаётся информация об ошибке.

Как понять

Скопировано

Промис решает задачу выполнения кода, который зависит от результата асинхронной операции.

Промис устроен таким образом, что рычаги управления его состоянием остаются у асинхронной функции. После создания, промис находится в состоянии ожидания pending. Когда асинхронная операция завершается, функция переводит промис в состояние успеха fulfilled или ошибки rejected.

С помощью методов then(), catch() и finally() мы можем реагировать на изменение состояния промиса и использовать результат его выполнения.

Пример ниже показывает состояние промиса, который создаётся при нажатии на кнопку купить. Промис случайным образом завершается успехом или ошибкой:

Открыть демо в новой вкладке

Методы

Скопировано

В работе мы чаще используем промисы, чем создаём. Использовать промис — значит выполнять код при изменении состояния промиса.

Существует три метода, которые позволяют работать с результатом выполнения вычисления внутри промиса: then(), catch() и finally().

Метод then() используют, чтобы выполнить код после успешного выполнения асинхронной операции.

Например, мы запросили у сервера список фильмов и хотим отобразить их на экране, когда от сервера придёт результат. В этом случае:

  • асинхронная операция — запрос данных у сервера;
  • код, который мы хотим выполнить после её завершения, — отрисовка списка.

Метод then() принимает в качестве аргумента две функции-колбэка. Если промис в состоянии fulfilled, выполнится первая функция. Если в состоянии rejected — вторая. Хорошей практикой считается не использовать второй аргумент метода then() и обрабатывать ошибки при помощи метода catch().

        
          
          fetch(`https://swapi.dev/api/films/${id}/`).then(function (movies) {  renderList(movies)})
          fetch(`https://swapi.dev/api/films/${id}/`).then(function (movies) {
  renderList(movies)
})

        
        
          
        
      

В коде выше, асинхронная функция fetch() возвращает промис, к которому применяется метод then. При его выполнении в переменной movies будет ответ сервера.

Подробнее о работе then() читайте в статье «Promise. Метод then()».

Метод catch() используют, чтобы выполнить код в случае ошибки при выполнении асинхронной операции.

Например, мы запросили у сервера список фильмов и хотим показать экран обрыва соединения, если произошла ошибка. В этом случае:

  • асинхронная операция — запрос данных у сервера;
  • код, который мы хотим выполнить при ошибке — экран обрыва соединения.

Метод catch() принимает в качестве аргумента функцию-колбэк, которая выполняется сразу после того, как промис поменял состояние на rejected. Параметр колбэка содержит экземпляр ошибки:

        
          
          fetch(`https://swapi.dev/api/films/${id}/`).catch(function (error) {  renderErrorMessage(error)})
          fetch(`https://swapi.dev/api/films/${id}/`).catch(function (error) {
  renderErrorMessage(error)
})

        
        
          
        
      

В коде выше асинхронная функция fetch() возвращает промис, к которому применяется метод catch(). При его выполнении в переменной error будет экземпляр ошибки.

Подробнее о работе catch() читайте в статье «Promise. Метод catch()».

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

Самый частый сценарий использования finally() — работа с индикаторами загрузки. Перед началом асинхронной операции разработчик включает индикатор загрузки. Индикатор нужно убрать вне зависимости от того, как завершилась операция. Если этого не сделать, то пользователь не сможет взаимодействовать с интерфейсом.

Метод finally() принимает в качестве аргумента функцию-колбэк, которая выполняется сразу после того, как промис поменял состояние на rejected или fulfilled:

        
          
          let isLoading = truefetch(`https://swapi.dev/api/films/${id}/`).finally(function () {  isLoading = false})
          let isLoading = true
fetch(`https://swapi.dev/api/films/${id}/`).finally(function () {
  isLoading = false
})

        
        
          
        
      

Подробнее о работе finally() читайте в статье «Promise. Метод finally()».

Цепочки методов

Скопировано

Методы then(), catch() и finally() часто объединяют в цепочки вызовов, чтобы обработать и успешный, и ошибочный сценарии:

        
          
          let isLoading = truefetch(`https://swapi.dev/api/films/${id}/`)  .then(function (movies) {    renderList(movies)  })  .catch(function (err) {    renderErrorMessage(err)  })  .finally(function () {    isLoading = false  })
          let isLoading = true

fetch(`https://swapi.dev/api/films/${id}/`)
  .then(function (movies) {
    renderList(movies)
  })
  .catch(function (err) {
    renderErrorMessage(err)
  })
  .finally(function () {
    isLoading = false
  })

        
        
          
        
      

При успешном завершении операции выполним код из then(), при ошибке — код из catch(). Затем выполнится код из finally().

Цепочки методов — очень гибкий подход. Он позволяет создавать зависимые асинхронные операции.

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

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

        
          
          fetch(`https://swapi.dev/api/films/${id}/`)  .then(function (response) {    // Cработает, когда разрешится промис с запросом данных о фильме.    // Следует распарсить ответ сервера: это асинхронная операция    return response.json()  })  .then(function (movie) {    // Сработает, когда данные о фильме распарсятся.    // Возвращаем промис из колбэка, чтобы продолжить цепочку    const characterUrl = movie.characters[0]    return fetch(characterUrl)  })  .then(function (response) {    // Сработает, когда разрешится промис с результатами запроса персонажа    return response.json()  })  .then(function (character) {    renderCharacterProfile(character)  })  .catch(function (err) {    // Сработает, когда любая из операций выше завершится ошибкой    renderErrorMessage(err)  })
          fetch(`https://swapi.dev/api/films/${id}/`)
  .then(function (response) {
    // Cработает, когда разрешится промис с запросом данных о фильме.
    // Следует распарсить ответ сервера: это асинхронная операция
    return response.json()
  })
  .then(function (movie) {
    // Сработает, когда данные о фильме распарсятся.
    // Возвращаем промис из колбэка, чтобы продолжить цепочку
    const characterUrl = movie.characters[0]
    return fetch(characterUrl)
  })
  .then(function (response) {
    // Сработает, когда разрешится промис с результатами запроса персонажа
    return response.json()
  })
  .then(function (character) {
    renderCharacterProfile(character)
  })
  .catch(function (err) {
    // Сработает, когда любая из операций выше завершится ошибкой
    renderErrorMessage(err)
  })

        
        
          
        
      

Обработка ошибок в цепочках методов

Скопировано
Открыть демо в новой вкладке

Цепочки then() при обработке промисов могут быть большими. В предыдущем примере цепочка состоит из четырёх then() и одного catch(). Как в этом случае отработает catch()?

catch() обрабатывает ошибки от всех then() между ним и предыдущим catch().

Один метод catch, поставленный в конце цепочки

Наш catch() — последний, а предыдущего нет, поэтому он будет обрабатывать все ошибки.

Несколько методов catch, поставленных в середине и конце цепочки

Если в цепочке несколько catch(), каждый ловит ошибки от then(), находящихся выше.

Один метод catch, поставленный в середине цепочки

Возможен вариант, когда финального catch() нет. Тогда ошибки от последних then() не будут обрабатываться.

Код с длинными цепочками — плохой. Если в одном из последних then() произойдёт ошибка, то вся дальнейшая цепочка не отработает. Из-за асинхронной природы промиса, остальной код вне промиса продолжит работать и приложение не упадёт.

Другие методы промисов

Скопировано

Иногда нужно обернуть уже известный результат вычисления в промис. Для этого используйте метод Promise.resolve():

        
          
          const happyDog = Promise.resolve('🐶')happyDog.then(function (dog) {  console.log(dog) // 🐶})
          const happyDog = Promise.resolve('🐶')

happyDog.then(function (dog) {
  console.log(dog) // 🐶
})

        
        
          
        
      

Другой метод Promise.reject() используется реже. Обратите внимание, что результатом выполнения sadDog.catch() будет промис в статусе fulfilled:

        
          
          const sadDog = Promise.reject('🐶')sadDog.catch(function (dog) {  console.log(dog) // 🐶})
          const sadDog = Promise.reject('🐶')

sadDog.catch(function (dog) {
  console.log(dog) // 🐶
})

        
        
          
        
      

Асинхронная функция с промисом

Скопировано

Создадим функцию, которая выполняет асинхронную операцию:

        
          
          function earnAllMoney() {}
          function earnAllMoney() {}

        
        
          
        
      

Вернём из функции только что созданный промис:

        
          
          function earnAllMoney() {  return new Promise(function (resolve, reject) { /* ... */ })}
          function earnAllMoney() {
  return new Promise(function (resolve, reject) { /* ... */ })
}

        
        
          
        
      

Передайте функцию в конструктор в качестве аргумента. Функция выполняет асинхронную операцию и переводит промис в состояние «успех» или «ошибка» в зависимости от результата:

        
          
          function earnAllMoney() {  return new Promise(function (resolve, reject) {    // Асинхронная операция    const result = tryEarnAllMoney()    if (result.ok) {      // Успех → переводим промис в fulfilled      // и передаём результат      resolve(result)    } else {      // Ошибка → переводим промис в rejected      reject(new Error(result))    }  })}
          function earnAllMoney() {
  return new Promise(function (resolve, reject) {
    // Асинхронная операция
    const result = tryEarnAllMoney()
    if (result.ok) {
      // Успех → переводим промис в fulfilled
      // и передаём результат
      resolve(result)
    } else {
      // Ошибка → переводим промис в rejected
      reject(new Error(result))
    }
  })
}

        
        
          
        
      

Рассмотрим пример с функцией getData. Функция принимает два колбэка: первый вызывается при успехе, второй — при ошибке. Код этой функции может выглядеть так:

        
          
          function getData(onSuccess, onError) {  setTimeout(function () {    const result = Math.random()    if (result > 0.5) {      onSuccess(result)    } else {      onError(new Error('Что-то пошло не так'))    }  }, 1000)}
          function getData(onSuccess, onError) {
  setTimeout(function () {
    const result = Math.random()
    if (result > 0.5) {
      onSuccess(result)
    } else {
      onError(new Error('Что-то пошло не так'))
    }
  }, 1000)
}

        
        
          
        
      

Завернём её в промис:

        
          
          function promisifiedGetData() {  return new Promise(function (resolve, reject) {    const result = getData(      function (result) {        resolve(result)      },      function (error) {        reject(error)      }    )  })}
          function promisifiedGetData() {
  return new Promise(function (resolve, reject) {
    const result = getData(
      function (result) {
        resolve(result)
      },
      function (error) {
        reject(error)
      }
    )
  })
}

        
        
          
        
      

Теперь используем методы then() и catch():

        
          
          promisifiedGetData()  .then(function () {    console.log('success')  })  .catch(function (err) {    console.error(err.message)  })
          promisifiedGetData()
  .then(function () {
    console.log('success')
  })
  .catch(function (err) {
    console.error(err.message)
  })

        
        
          
        
      

Схлопывание промисов

Скопировано

Интересная и удобная особенность промисов – если вложить один промис в другой они схлопнутся в один. Например:

        
          
          const promise = Promise.resolve(Promise.resolve(Promise.resolve('🐶')))// Promise {<fulfilled>: '🐶'}promise.then(console.log)// 🐶
          const promise = Promise.resolve(Promise.resolve(Promise.resolve('🐶')))
// Promise {<fulfilled>: '🐶'}
promise.then(console.log)
// 🐶

        
        
          
        
      

На практике

Скопировано

Николай Лопин советует

Скопировано

🛠 Промис становится «разрешённым» или «завершённым», когда он переходит из состояние pending в fulfilled или rejected. Состояние завершённого промиса нельзя поменять.

        
          
          const promise = new Promise(function (resolve, reject) {  resolve() // Тут промис переходит в состояние fulfilled  reject() // Вызов игнорируется, потому что промис разрешился})
          const promise = new Promise(function (resolve, reject) {
  resolve() // Тут промис переходит в состояние fulfilled
  reject() // Вызов игнорируется, потому что промис разрешился
})

        
        
          
        
      

🛠 Всегда завершайте использование промиса методом catch(). Если этого не сделать, следующие промисы в цепочке перестанут работать. Такую ошибку поймаете только через специальный обработчик unhandledrejection.

🛠 Время от времени нужно выполнить несколько асинхронных функций и дождаться, пока все выполнятся или одна из них завершится ошибкой. Для этого существует статический метод Promise.all() (возвращает промис).

🛠 Если нужно дождаться, пока несколько асинхронных функций завершатся (без разницы, успешно или ошибкой), используйте метод Promise.allSettled() (вернёт промис).