Разбираемся, как работать с 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) })
Комментарии