Промисы являются частью стандарта ECMAScript 2015 (или ES6, поэтому их также называют промисами ES6) и изначально доступны в Node.js, начиная с версии 4. Но история промисов восходит к нескольким годам ранее, когда десятки реализаций вокруг, изначально с разными функциями и поведением. В конце концов, большинство этих реализаций остановились на стандарте под названием Promises/A+.

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

Что такое обещание?

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

Чтобы получить значение выполнения или ошибку (причину), связанную с отклонением, мы можем использовать метод then() экземпляра Promise. Вот его подпись:

promise.then(onFulfilled, onRejected)

В предыдущей сигнатуре onFulfilled — это обратный вызов, который в конечном итоге получит значение выполнения Promise, а onRejected — это еще один обратный вызов, который получит причина отказа (если есть). Оба являются необязательными.

Чтобы иметь представление о том, как промисы могут преобразовать наш код, давайте рассмотрим следующий код на основе обратного вызова:

asyncOperation(arg, (err, result) => {
   if(err) {
     // handle the error
   }
     // do stuff with the result
})

Промисы позволяют нам преобразовать этот типичный код стиля с передачей продолжения в более структурированный и элегантный код, такой как следующий:

asyncOperationPromise(arg)
   .then(result => {
     // do stuff with result
   }, err => {
     // handle the error
   })

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

Кроме того, если какая-либо из функций onFulfilled или onRejected возвращает значение x, обещание, возвращаемое методом then(), будет:

  • Выполнить с x, если x является значением
  • Выполнить со значением выполнения x, если x является обещанием
  • Отклонить с возможной причиной отклонения x, если x является обещанием

Такое поведение позволяет нам строить цепочки промисов, позволяя легко агрегировать и упорядочивать асинхронные операции в несколько конфигураций. Более того, если мы не укажем обработчик onFulfilled или onRejected, значение выполнения или причина отклонения автоматически перенаправляется на следующее обещание в цепочке. Это позволяет нам, например, автоматически распространять ошибки по всей цепочке, пока они не будут перехвачены обработчиком onRejected. С цепочкой промисов последовательное выполнение задач вдруг становится тривиальной операцией:

asyncOperationPromise(arg)
 .then(result1 => {
   // returns another promise
   return asyncOperationPromise(arg2)
 })
 .then(result2 => {
   // returns a value
   return 'done'
 })
then(undefined err => {
 .then(undefined, err => {
   // any error in the chain is caught here
 })

На следующей диаграмме показан другой взгляд на то, как работает цепочка промисов:

На рисунке выше показано, как работает наша программа, когда мы используем цепочку промисов. Когда мы вызываем then() для промиса A, мы синхронно получаем в результате промис B, а когда мы вызываем then() в промисе B, мы синхронно получаем в результате промис C. В конце концов, когда Promise A устанавливается, оно либо выполняется, либо отклоняется, что приводит к вызову обратного вызова onFulfilled() или onRejected() соответственно. Результат выполнения такого обратного вызова затем выполнит или отклонит обещание B, и такой результат, в свою очередь, распространяется на обратный вызов onFulfilled() или onRejected(), переданный в вызов then() для обещания B. аналогично продолжается для промиса C и любого другого промиса, следующего за ним в цепочке.

Важным свойством промисов является то, что обратные вызовы onFulfilled() и onRejected() гарантированно будут вызываться асинхронно и не более одного раза, даже если мы разрешим промис синхронно со значением. Мало того, обратные вызовы onFulfilled() и onRejected() будут вызываться асинхронно, даже если объект Promise уже урегулирован в момент, когда вызывается then(). Такое поведение защищает наш код от всех тех ситуаций, когда мы можем непреднамеренно выпустить Zalgo, делая наш асинхронный код более согласованным и надежным без каких-либо дополнительных усилий.

Теперь наступает лучшая часть. Если в обработчике onFulfilled() или onRejected() генерируется исключение (используя оператор throw), обещание, возвращаемое методом then(), автоматически отклоняется, при этом в качестве причины отклонения указывается выброшенное исключение. Это огромное преимущество по сравнению с CPS, так как это означает, что с промисами исключения будут автоматически распространяться по цепочке, и оператор throw становится наконец пригодным для использования.