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());
Взаимоотношения вызовов в коде выше можно описать такой схемой:
Важно также, что 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)
})