9 полезных советов по Promise.resolve и Promise.reject
Разбираемся, как работать с Promise.resolve, где лучше обрабатывать исключения Promise.reject, и какие еще есть хитрости в работе с асинхронным JS.
1. Вернуть Promise можно внутри .then
Возвращенный promise сразу будет готов к использованию в следующем .then:
1
2
3
4
5
6
|
.then(r => {
return serverStatusPromise(r); // { statusCode: 200 }
})
.then(resp => {
console.log(resp.statusCode); // 200; помните об автоматическом разворачивании промисов
})
|
2. Новый Promise создается при каждом .then
Если вы знакомы с цепочкой вызовов в JavaScript, то вы и так все знаете. Тем же, кто не в курсе, как это работает, некоторые вещи могут показаться не очевидными.
Каждый раз, когда вы используете .then или .catch к промисам, вы создаете новый. Новая сущность представляет собой композицию промиса и вызова .then или .catch, который был привязан.
На примере:
1
2
3
4
5
6
7
|
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. Это также означает, что никто не сможет изменить ваш промис, так что его можно передавать без опаски.
1
2
3
4
5
6
7
8
9
10
11
12
|
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-конструкторов, которые вы используете.
Вот реальный случай использования конструктора:
1
2
3
4
5
6
|
return new Promise((res, rej) => {
fs.readFile(«/etc/passwd», function(err, data) {
if (err) return rej(err);
return res(data);
});
});
|
Promise constructor понадобится только в том случае, когда вам нужно конвертировать callback в промис.
Однажды овладев этим способом создания промисов, велик будет соблазн использовать его всюду.
Вот пример ненужного использования конструктора:
1
2
3
4
5
6
7
8
|
return new Promise((res, rej) => {
var fetchPromise = fetchSomeData(.....);
fetchPromise
.then(data => {
res(data);
})
.catch(err => rej(err))
})
|
А правильно так:
1
|
return fetchSomeData(...);
|
Заворачивание Promise в конструктор излишне и убивает всю пользу.
Если вы работаете с Node.js, присмотритесь к util-promisify. Эта маленькая штука поможет преобразовывать колбеки Node.js в промисы.
1
2
3
4
5
6
7
8
|
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 помогает сокращать некоторые сложные конструкции, как в примере:
1
2
3
|
var similarProm = new Promise(res => res(5));
// ^^ то же самое
var prom = Promise.resolve(5);
|
У .resolve множество вариантов применения, один из них помогает конвертировать обычный JS-объект в promise:
1
2
3
4
|
// конвертируем синхронную функцию в асинхронную
function foo() {
return Promise.resolve(5);
}
|
Этот метод можно использовать как оболочку для значений, когда точно не известно промис это или простое значение.
1
2
3
4
5
6
7
8
9
10
11
12
|
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 может послужить заменой такому коду:
1
2
3
|
var rejProm = new Promise((res, reject) => reject(5));
rejProm.catch(e => console.log(e)) // 5
|
Функционал Promise.reject полностью оправдывает свое имя – он нужен чтобы отклонить промис.
1
2
3
4
5
6
7
8
|
function foo(myVal) {
if (!mVal) {
return Promise.reject(new Error(‘myVal is required’))
}
return new Promise((res, rej) => {
// конвертация колбека
})
}
|
В примере ниже reject используется внутри .then:
1
2
3
4
5
6
|
.then(val => {
if (val != 5) {
return Promise.reject(‘Not Good’);
}
})
.catch(e => console.log(e)) // Not Good
|
7. Использование Promise.all
Алгоритм работы Promise.all можно описать примерно так:
1
2
3
4
|
Принимает множество Promise
затем ждет, пока все они завершатся
затем возвращает новый промис, который разрешается в массив
улавливает ошибку, если один из них отклоняется
|
Следующий пример показывает, когда все промисы разрешаются:
1
2
3
4
5
6
7
8
|
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
})
|
Этот пример показывает, когда один из них терпит неудачу:
1
2
3
4
5
6
7
8
9
|
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 должны обрабатываться там.
Не стесняйтесь писать код вроде этого:
1
|
return fetchSomeData(...);
|
В этом случае, чтобы обработать rejection функции, нужно решить, разрешать работу как есть или продолжить обработку отказа. Есть небольшая уловка при работе с catch. Если вернуть Promise.reject в catch, то он будет отклонен.
1
2
3
4
5
6
7
8
9
10
|
.then(() => 5.length) // <— что-то не так
.catch(e => {
return 5; // <— решаем проблему
})
.then(r => {
console.log(r); // 5
})
.catch(e => {
console.error(e); // эта функция не будет вызвана
})
|
Чтобы отклонить reject достаточно ничего не делать. Пусть это будет проблемой других функций. Чаще всего родительские функции имеют больше возможностей для обработки отказов, чем функция, в которой произошел reject.
Важно помнить, что если вы пишете .catch, значит, собираетесь обрабатывать ошибки. Если нужно перехватить отказ:
1
2
3
4
5
6
7
8
9
10
11
12
|
.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), но эти обработчики по разному обрабатывают ошибки.
1
2
3
4
5
6
7
8
9
10
11
12
|
.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.
Неверно:
1
2
3
4
5
6
7
8
|
request(opts)
.catch(err => {
if (err.statusCode === 400) {
return request(opts)
.then(r => r.text())
.catch(err2 => console.error(err2))
}
})
|
Верно:
1
2
3
4
5
6
7
8
9
|
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, и нет других вариантов, кроме цепочки вызовов.
1
2
3
4
5
6
7
8
|
.then(myVal => {
const promA = foo(myVal);
const promB = anotherPromMake(myVal);
return promA
.then(valA => {
return promB.then(valB => hungryFunc(valA, valB));
})
})
|
Можно использовать ES6 подход к деструктуризации и Promise.all:
1
2
3
4
5
6
7
8
9
|
.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)
})
|

