9 полезных советов по Promise.resolve и Promise.reject

Разбираемся, как работать с Promise.resolve, где лучше обрабатывать исключения Promise.reject, и какие еще есть хитрости в работе с асинхронным JS.

1. Вернуть Promise можно внутри .then

Возвращенный promise сразу будет готов к использованию в следующем .then:

.then(r => {
    return serverStatusPromise(r); // { statusCode: 200 }
})
.then(resp => {
    console.log(resp.statusCode); // 200; помните об автоматическом разворачивании промисов
})

2. Новый Promise создается при каждом .then

Если вы знакомы с цепочкой вызовов в JavaScript, то вы и так все знаете. Тем же, кто не в курсе, как это работает, некоторые вещи могут показаться не очевидными.

Каждый раз, когда вы используете .then или .catch к промисам, вы создаете новый. Новая сущность представляет собой композицию промиса и вызова .then или .catch, который был привязан.

На примере:

var statusProm = fetchServerStatus();

var promA = statusProm.then(r => (r.statusCode === 200 ? "good" : "bad"));

var promB = promA.then(r => (r === "good" ? "ALL OK" : "NOTOK"));

var promC = statusProm.then(r => fetchThisAnotherThing());

Взаимоотношения вызовов в коде выше можно описать такой схемой:

Схема Promise

Важно также, что promA, promB и promC – разные промисы, но родственные.

3. Оповещения о resolve/reject доступны везде

Если один промис используют несколько частей приложения, каждая часть будет оповещена, когда он получит состояние resolve/reject. Это также означает, что никто не сможет изменить ваш промис, так что его можно передавать без опаски.

function yourFunc() {
  const yourAwesomeProm = makeMeProm();

  yourEvilUncle(yourAwesomeProm); // будьте уверены, промис будет работать несмотря на то,
// как его употребит злой дядя

  return yourAwesomeProm.then(r => importantProcessing(r));
}

function yourEvilUncle(prom) {
  return prom.then(r => Promise.reject("destroy!!"));
}

В примере выше видно, что Promise по определению трудно изменяем.

4. Конструктор промисов не решает проблему

Многие разработчики повсеместно используют конструктор с мыслью, что все делают правильно. В действительности, API конструктора очень похоже на старое доброе Callback API.

Чтобы на самом деле отойти от callback’ов, необходимо уменьшить количество promise-конструкторов, которые вы используете.

Вот реальный случай использования конструктора:

return new Promise((res, rej) => {
  fs.readFile("/etc/passwd", function(err, data) {
    if (err) return rej(err);
    return res(data);
  });
});

Promise constructor понадобится только в том случае, когда вам нужно конвертировать callback в промис.

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

Вот пример ненужного использования конструктора:

return new Promise((res, rej) => {
    var fetchPromise = fetchSomeData(.....);
    fetchPromise
        .then(data => {
            res(data);
        })
        .catch(err => rej(err))
})

А правильно так:

return fetchSomeData(...);

Заворачивание Promise в конструктор излишне и убивает всю пользу.

Если вы работаете с Node.js, присмотритесь к util-promisify. Эта маленькая штука поможет преобразовывать колбеки Node.js в промисы.

const {promisify} = require('util');
const fs = require('fs');

const readFileAsync = promisify(fs.readFile);

readFileAsync('myfile.txt', 'utf-8')
  .then(r => console.log(r))
  .catch(e => console.error(e));

5. Использование Promise.resolve

Promise.resolve помогает сокращать некоторые сложные конструкции, как в примере:

var similarProm = new Promise(res => res(5));
// ^^ то же самое
var prom = Promise.resolve(5);

У .resolve множество вариантов применения, один из них помогает конвертировать обычный JS-объект в promise:

// конвертируем синхронную функцию в асинхронную
function foo() {
  return Promise.resolve(5);
}

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

function goodProm(maybePromise) {
  return Promise.resolve(maybePromise);
}

goodProm(5).then(console.log); // 5

var sixPromise = fetchMeNumber(6); // промис, который разрешается в 5

goodProm(sixPromise).then(console.log); // 6

goodProm(Promise.resolve(Promise.resolve(5))).then(console.log);
// 5, обратите внимание, что все слои промиса будут автоматически развернуты

6. Использование Promise.reject

Promise.reject может послужить заменой такому коду:

var rejProm = new Promise((res, reject) => reject(5));

rejProm.catch(e => console.log(e)) // 5

Функционал Promise.reject полностью оправдывает свое имя – он нужен чтобы отклонить промис.

function foo(myVal) {
    if (!mVal) {
        return Promise.reject(new Error('myVal is required'))
    }
    return new Promise((res, rej) => {
        // конвертация колбека
    })
}

В примере ниже reject используется внутри .then:

.then(val => {
  if (val != 5) {
    return Promise.reject('Not Good');
  }
})
.catch(e => console.log(e)) // Not Good

7. Использование Promise.all

Алгоритм работы Promise.all можно описать примерно так:

Принимает множество Promise
    затем ждет, пока все они завершатся
    затем возвращает новый промис, который разрешается в массив
    улавливает ошибку, если один из них отклоняется

Следующий пример показывает, когда все промисы разрешаются:

var prom1 = Promise.resolve(5);
var prom2 = fetchServerStatus(); // вернет промис {statusCode: 200}

Proimise.all([prom1, prom2])
.then([val1, val2] => { // на выходе будет массив
    console.log(val1); // 5
    console.log(val2.statusCode); // 200
})

Этот пример показывает, когда один из них терпит неудачу:

var prom1 = Promise.reject(5);
var prom2 = fetchServerStatus(); // вернет промис {statusCode: 200}

Proimise.all([prom1, prom2])
.then([val1, val2] => {
    console.log(val1); 
    console.log(val2.statusCode); 
})
.catch(e =>  console.log(e)) // 5, перейдет сразу к .catch

8. Обрабатывайте отказы уровнем выше

Оставляйте решение проблем с rejection родительским функциям. В идеале, обработка  отказов должна находиться в корне приложения, и все Promise.reject должны обрабатываться там.

Не стесняйтесь писать код вроде этого:

return fetchSomeData(...);

В этом случае, чтобы обработать rejection функции, нужно решить, разрешать работу как есть или продолжить обработку отказа. Есть небольшая уловка при работе с catch. Если вернуть Promise.reject в catch, то он будет отклонен.

.then(() => 5.length) // <-- что-то не так
.catch(e => {
        return 5;  // <-- решаем проблему
})
.then(r => {
    console.log(r); // 5
})
.catch(e => {
    console.error(e); // эта функция не будет вызвана
})

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

Важно помнить, что если вы пишете .catch, значит, собираетесь обрабатывать ошибки. Если нужно перехватить отказ:

.then(() => 5.length) // <-- что-то не так
.catch(e => {
  errorLogger(e); // неявное действие
  return Promise.reject(e); // отклоняется
})
.then(r => {
    console.log(r); // этот .then (или любой последующий) никогда не будет вызван,
// так как был отклонен выше
})
.catch(e => {
    console.error(e); //<-- будет разрешатся текущим .catch
})

.then принимает второй параметр, который можно использовать для обработки ошибок. Это может напомнить then(x).catch(x), но эти обработчики по разному обрабатывают ошибки.

.then(function() {
   return Promise.reject(new Error('something wrong happened'));
}).catch(function(e) {
   console.error(e); // что-то не так
});


.then(function() {
   return Promise.reject(new Error('something wrong happened'));
}, function(e) { // callback обрабатывает ошибку из цепочки текущего .then
    console.error(e); // ошибок не будет
});

9. Избегайте нагромождений .then

Совет довольно прост: избегайте использования .then внутри .then или .catch.

Неверно:

request(opts)
.catch(err => {
  if (err.statusCode === 400) {
    return request(opts)
           .then(r => r.text())
           .catch(err2 => console.error(err2))
  }
})

Верно:

request(opts)
.catch(err => {
  if (err.statusCode === 400) {
    return request(opts);
  }
  return Promise.reject(err);
})
.then(r => r.text())
.catch(err => console.erro(err));

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

.then(myVal => {
    const promA = foo(myVal);
    const promB = anotherPromMake(myVal);
    return promA
          .then(valA => {
              return promB.then(valB => hungryFunc(valA, valB));
          })
})

Можно использовать ES6 подход к деструктуризации и Promise.all:

.then(myVal => {
    const promA = foo(myVal);
    const promB = anotherPromMake(myVal);
    return Promise.all([prom, anotherProm])
})
.then(([valA, valB]) => {   // ES6
    console.log(valA, valB) // все разрешенные значения
    return hungryFunc(valA, valB)
})

Больше полезного по JS:

Комментарии

ВАКАНСИИ

Добавить вакансию
Разработчик C++
Москва, по итогам собеседования

ЛУЧШИЕ СТАТЬИ ПО ТЕМЕ