Использование промисов
- « Предыдущая статья
- Следующая статья »
Promise (промис) — это объект, представляющий результат успешного или неудачного завершения асинхронной операции. Так как большинство людей пользуются уже созданными промисами, это руководство начнём с объяснения использования вернувшихся промисов до объяснения принципов создания.
В сущности, промис — это возвращаемый объект, в который вы записываете два колбэка вместо того, чтобы передать их функции.
Например, вместо старомодной функции, которая принимает два колбэка и вызывает один из них в зависимости от успешного или неудачного завершения операции:
function doSomethingOldStyle(successCallback, failureCallback) {
console.log("Готово.");
// Успех в половине случаев.
if (Math.random() > .5) {
successCallback("Успех")
} else {
failureCallback("Ошибка")
}
}
function successCallback(result) {
console.log("Успешно завершено с результатом " + result);
}
function failureCallback(error) {
console.log("Завершено с ошибкой " + error);
}
doSomethingOldStyle(successCallback, failureCallback);
…современные функции возвращают промис, в который вы записываете ваши колбэки:
function doSomething() {
return new Promise((resolve, reject) => {
console.log("Готово.");
// Успех в половине случаев.
if (Math.random() > .5) {
resolve("Успех")
} else {
reject("Ошибка")
}
})
}
const promise = doSomething();
promise.then(successCallback, failureCallback);
…или просто:
doSomething().then(successCallback, failureCallback);
Мы называем это асинхронным вызовом функции. У этого соглашения есть несколько преимуществ. Давайте рассмотрим их.
Гарантии
В отличие от старомодных переданных колбэков промис даёт некоторые гарантии:
- Колбэки никогда не будут вызваны до завершения обработки текущего события в событийном цикле JavaScript.
- Колбэки, добавленные через .then даже после успешного или неудачного завершения асинхронной операции, будут также вызваны.
- Несколько колбэков может быть добавлено вызовом .then нужное количество раз, и они будут выполняться независимо в порядке добавления.
Но наиболее непосредственная польза от промисов — цепочка вызовов (chaining).
Цепочка вызовов
Общая нужда — выполнять две или более асинхронных операции одна за другой, причём каждая следующая начинается при успешном завершении предыдущей и использует результат её выполнения. Мы реализуем это, создавая цепочку вызовов промисов (promise chain).
Вот в чём магия: функция then возвращает новый промис, отличающийся от первоначального:
let promise = doSomething();
let promise2 = promise.then(successCallback, failureCallback);
или
let promise2 = doSomething().then(successCallback, failureCallback);
Второй промис представляет завершение не только doSomething(), но и функций successCallback или failureCallback, переданных вами, а они тоже могут быть асинхронными функциями, возвращающими промис. В этом случае все колбэки, добавленные к promise2 будут поставлены в очередь за промисом, возвращаемым successCallback или failureCallback.
По сути, каждый вызванный промис означает успешное завершение предыдущих шагов в цепочке.
Раньше выполнение нескольких асинхронных операций друг за другом приводило к классической «Вавилонской башне» колбэков:
doSomething(function(result) {
doSomethingElse(result, function(newResult) {
doThirdThing(newResult, function(finalResult) {
console.log('Итоговый результат: ' + finalResult);
}, failureCallback);
}, failureCallback);
}, failureCallback);
В современных функциях мы записываем колбэки в возвращаемые промисы — формируем цепочку промисов:
doSomething().then(function(result) {
return doSomethingElse(result);
})
.then(function(newResult) {
return doThirdThing(newResult);
})
.then(function(finalResult) {
console.log('Итоговый результат: ' + finalResult);
})
.catch(failureCallback);
Аргументы then необязательны, а catch(failureCallback) — это сокращение для then(null, failureCallback). Вот как это выражено с помощью стрелочных функций:
doSomething()
.then(result => doSomethingElse(result))
.then(newResult => doThirdThing(newResult))
.then(finalResult => {
console.log(`Итоговый результат: ${finalResult}`);
})
.catch(failureCallback);
Важно: Всегда возвращайте промисы в return, иначе колбэки не будут сцеплены и ошибки могут быть не пойманы (стрелочные функции неявно возвращают результат, если скобки {} вокруг тела функции опущены).
Цепочка вызовов после catch
Можно продолжить цепочку вызовов после ошибки, т. е. после catch, что полезно для выполнения новых действий даже после того, как действие вернёт ошибку в цепочке вызовов. Ниже приведён пример:
new Promise((resolve, reject) => {
console.log('Начало');
resolve();
})
.then(() => {
throw new Error('Где-то произошла ошибка');
console.log('Выведи это');
})
.catch(() => {
console.log('Выведи то');
})
.then(() => {
console.log('Выведи это, несмотря ни на что');
});
В результате выведется данный текст:
Начало Выведи то Выведи это, несмотря ни на что
Заметьте, что текст «Выведи это» не вывелся, потому что «Где-то произошла ошибка» привела к отказу
Распространение ошибки
Вы могли ранее заметить, что failureCallback повторяется три раза в «pyramid of doom», а в цепочке промисов всего лишь один раз:
doSomething()
.then(result => doSomethingElse(result))
.then(newResult => doThirdThing(newResult))
.then(finalResult => console.log(`Итоговый результат: ${finalResult}`))
.catch(failureCallback);
В основном, цепочка промисов останавливает выполнение кода, если где-либо произошла ошибка, и вместо этого ищет далее по цепочке обработчики ошибок. Это очень похоже на то, как работает синхронный код:
try {
let result = syncDoSomething();
let newResult = syncDoSomethingElse(result);
let finalResult = syncDoThirdThing(newResult);
console.log(`Итоговый результат: ${finalResult}`);
} catch(error) {
failureCallback(error);
}
Эта симметрия с синхронным кодом лучше всего показывает себя в синтаксическом сахаре async/await в ECMAScript 2017:
async function foo() {
try {
let result = await doSomething();
let newResult = await doSomethingElse(result);
let finalResult = await doThirdThing(newResult);
console.log(`Итоговый результат: ${finalResult}`);
} catch(error) {
failureCallback(error);
}
}
Работа данного кода основана на промисах. Для примера здесь используется функция doSomething(), которая встречалась ранее. Вы можете прочитать больше о синтаксисе здесь
Промисы решают основную проблему пирамид, обработку всех ошибок, даже вызовов исключений и программных ошибок. Это основа для функционального построения асинхронных операций.
Создание промиса вокруг старого колбэка
Promise может быть создан с помощью конструктора. Это может понадобится только для старых API.
В идеале, все асинхронные функции уже должны возвращать промис. Но увы, некоторые APIs до сих пор ожидают успешного или неудачного колбэка переданных по старинке. Типичный пример: setTimeout() функция:
setTimeout(() => saySomething("10 seconds passed"), 10000);
Смешивание старого колбэк-стиля и промисов проблематично. В случае неудачного завершения saySomething или программной ошибки, нельзя обработать ошибку.
К счастью мы можем обернуть функцию в промис. Хороший тон оборачивать проблематичные функции на самом низком возможном уровне, и больше никогда их не вызывать напрямую:
const wait = ms => new Promise(resolve => setTimeout(resolve, ms));
wait(10000).then(() => saySomething("10 seconds")).catch(failureCallback);
В сущности, конструктор промиса становится исполнителем функции, который позволяет нам резолвить или режектить промис вручную. Так как setTimeout всегда успешен, мы опустили reject в этом случае.
Композиция
Promise.resolve() и Promise.reject() короткий способ создать уже успешные или отклонённые промисы соответственно. Это иногда бывает полезно.
Promise.all() и Promise.race() — два метода запустить асинхронные операции параллельно.
Последовательное выполнение композиции возможно при помощи хитрости JavaScript:
[func1, func2].reduce((p, f) => p.then(f), Promise.resolve());
Фактически, мы превращаем массив асинхронных функций в цепочку промисов равносильно: Promise.resolve().then(func1).then(func2);
Это также можно сделать, объединив композицию в функцию, в функциональном стиле программирования:
const applyAsync = (acc,val) => acc.then(val); const composeAsync = (...funcs) => x => funcs.reduce(applyAsync, Promise.resolve(x));
composeAsync функция примет любое количество функций в качестве аргументов и вернёт новую функцию которая примет в параметрах начальное значение, переданное по цепочке. Это удобно, потому что некоторые или все функции могут быть либо асинхронными, либо синхронными, и они гарантированно выполнятся в правильной последовательности:
const transformData = composeAsync(func1, asyncFunc1, asyncFunc2, func2); transformData(data);
В ECMAScript 2017, последовательные композиции могут быть выполнены более простым способом с помощью async/await:
for (const f of [func1, func2]) {
await f();
}
Порядок выполнения
Чтобы избежать сюрпризов, функции, переданные в then никогда не будут вызваны синхронно, даже с уже разрешённым промисом:
Promise.resolve().then(() => console.log(2)); console.log(1); // 1, 2
Вместо немедленного выполнения, переданная функция встанет в очередь микрозадач, а значит выполнится, когда очередь будет пустой в конце текущего вызова JavaScript цикла событий (event loop), т.е. очень скоро:
const wait = ms => new Promise(resolve => setTimeout(resolve, ms)); wait().then(() => console.log(4)); Promise.resolve().then(() => console.log(2)).then(() => console.log(3)); console.log(1); // 1, 2, 3, 4
Вложенность
Простые цепочки promise лучше оставлять без вложений, так как вложенность может быть результатом небрежной структуры. Смотрите распространённые ошибки.
Вложенность — это управляющая структура, ограничивающая область действия операторов catch. В частности, вложенный catch только перехватывает сбои в своей области и ниже, а не ошибки выше в цепочке за пределами вложенной области. При правильном использовании это даёт большую точность в извлечение ошибок:
doSomethingCritical()
.then(result => doSomethingOptional()
.then(optionalResult => doSomethingExtraNice(optionalResult))
.catch(e => {})) // Игнорируется если необязательные параметр не выкинул исключение
.then(() => moreCriticalStuff())
.catch(e => console.log("Критическая ошибка: " + e.message));
Обратите внимание, что необязательный шаги здесь выделены отступом.
Внутренний оператор catch нейтрализует и перехватывает ошибки только от doSomethingOptional() и doSomethingExtraNice(), после чего код возобновляется с помощью moreCriticalStuff(). Важно, что в случае сбоя doSomethingCritical() его ошибка перехватывается только последним (внешним) catch.
Частые ошибки
В этом разделе собраны частые ошибки, возникающие при создании цепочек промисов. Несколько таких ошибок можно увидеть в следующем примере:
// Плохой пример! Три ошибки!
doSomething().then(function(result) {
doSomethingElse(result) // Забыл вернуть промис из внутренней цепочки + неуместное влаживание
.then(newResult => doThirdThing(newResult));
}).then(() => doFourthThing());
// Забыл закончить цепочку методом catch
Первая ошибка это неправильно сцепить вещи между собой. Такое происходит когда мы создаём промис но забываем вернуть его. Как следствие, цепочка сломана, но правильнее было бы сказать что теперь у нас есть две независимые цепочки, соревнующиеся за право разрешится первой. Это означает, что doFourthThing() не будет ждать doSomethingElse() или doThirdThing() пока тот закончится, и будет исполнятся параллельно с ними, это, вероятно, не то что хотел разработчик. Отдельные цепочки также имеют отдельную обработку ошибок, что приводит к необработанным ошибкам.
Вторая ошибка это излишняя вложенность, включая первую ошибку. Вложенность также ограничивает область видимости внутренних обработчиков ошибок, если это не то чего хотел разработчик, это может привести к необработанным ошибкам. Примером этого является пример как не нужно создавать промисы, который комбинирует вложенность с чрезмерным использованием конструктора промисов для оборачивания кода который уже использует промисы.
Третья ошибка это забыть закончить цепочку ключевым словом catch. Незаконченные цепочки приводят к необработанным отторжениям промисов в большинстве браузеров.
Хорошим примером является всегда либо возвращать либо заканчивать цепочки промисов, и как только вы получаете новый промис, возвращайте его сразу же, чтобы не усложнять код излишней вложенностью:
doSomething()
.then(function(result) {
return doSomethingElse(result);
})
.then(newResult => doThirdThing(newResult))
.then(() => doFourthThing())
.catch(error => console.log(error));
Обратите внимание что () => x это сокращённая форма () => { return x; }.
Теперь у нас имеется единственная определённая цепочка с правильной обработкой ошибок.
Использование async/await предотвращает большинство, если не все вышеуказанные ошибки, но взамен появляется другая частая ошибка — забыть ключевое слово await.
Смотрите также
Последнее обновление: 30.08.2021
Одним из преимуществ промисов является более простая обработка ошибок. Для получения и обработки ошибки мы можем использовать
функцию catch() объекта Promise, которая в качестве параметра принимает функцию обработчика ошибки:
const myPromise = new Promise(function(resolve, reject){
console.log("Выполнение асинхронной операции");
reject("Переданы некорректные данные");
});
myPromise.catch( function(error){
console.log(error);
});
Функция catch() в качестве параметра принимает обработчик ошибки. Параметром этой функции-обработчика является то значение,
которое передается в reject().
Консольный вывод:
Выполнение асинхронной операции Переданы некорректные данные
Генерация ошибки
Выше для извещения о возникшей ошибке вызывалась функция reject(). Но стоит отметить, что ошибка может возникнуть и без вызова функции
reject(). И если в выполняемой в промисе операции генерируется ошибка в силу тех или иных причин, то вся операция также завершается ошибкой.
Например, в следующем коде вызывается нигде не определенная функция getSomeWork():
const myPromise = new Promise(function(resolve){
console.log("Выполнение асинхронной операции");
getSomeWork(); // вызов не существующей функции
resolve("Hello world!");
});
myPromise.catch( function(error){
console.log(error);
});
Поскольку функция getSomeWork() нигде не объявлена, то выполнение асинхронной задачи завершится ошибкой и не дойдет до вызова resolve("Hello world!").
Поэтому сработает функция обработки ошибок из catch(), которая через параметр error получит информацию о возникшей ошибке, и
в консоли браузера мы увидим сообщение об ошибке:
Выполнение асинхронной операции
ReferenceError: getSomeWork is not defined
at index.html:39
at new Promise (<anonymous>)
at index.html:37
throw
Также ошибка может быть результатом вызова оператора throw, который генерирует ошибку:
cconst myPromise = new Promise(function(resolve, reject){
console.log("Выполнение асинхронной операции");
const parsed = parseInt("Hello");
if (isNaN(parsed)) {
throw "Not a number"; // Генерируем ошибку
}
resolve(parsed);
});
myPromise.catch( function(error){
console.log(error);
});
Здесь парсится в число случайная строка. И если результат парсинга не представляет число, то с помощью оператора throw генерируем ошибку.
Это придет к завершению всей функции с ошибкой. И в итоге результат будет обработан функцией catch:
Выполнение асинхронной операции Not a number
В этом случае функция обработчика получает сообщение об оошибке, который указывается после оператора throw.
Данная ситуация может показаться искуственной, так как нам нет смысла генерировать в коде выше ошибку с помощью throw, поскольку в этом случае мы также
можем передать сообщение об ошибке в функцию reject:
if (isNaN(parsed)) {
reject("Not a number");
}
Однако данный оператор может применяться во внешней функции, которую мы вызываем в коде:
function getNumber(str){
const parsed = parseInt(str);
if (isNaN(parsed)) throw "Not a number"; // Генерируем ошибку
else return parsed;
}
const myPromise = new Promise(function(resolve){
console.log("Выполнение асинхронной операции");
const result = getNumber("hello");
resolve(result);
});
myPromise.catch( function(error){
console.log(error);
});
Здесь парсинг строки в число вынесен во внешнюю функцию — getNumber, однако при вызове этой функции в промисе, также из оператора throw возникнет ошибка.
И соответственно будет выполняться функция catch(), где роизойдет обработка ошибки.
try..catch
Как и в общем случае, операции, которые могут генерировать ошибку, можно помещать в конструкцию try..catch, а при возникновении исключения в блоке catch вызывать функцию reject():
const myPromise = new Promise(function(resolve, reject){
try{
console.log("Выполнение асинхронной операции");
getSomeWork(); // вызов не существующей функции
resolve("Hello world!");
}
catch(err){
reject(`Произошла ошибка: ${err.message}`);
}
});
myPromise.catch( function(error){
console.log(error);
});
Консольный вывод:
Выполнение асинхронной операции Произошла ошибка: getSomeWork is not defined
Обработка ошибки с помощью функции then
Кроме функции catch для получения информации об ошибке и ее обработки также можно использовать функцию
then() — ее второй параметр представляет обработчик ошибки, который в качестве параметра получает переданное из функции
reject значение:
promise
.then(function(value){
// получение значения
},
function(error){
// обработка ошибки
});
Второй параметр функции then() представляет функцию обработчика ошибок. С помощью параметра error в функции-обработчика мы можем получить переданное в reject() значение, либо информацию о возникшей ошибке.
Рассмотрим следуюший пример:
function generateNumber(str){
return new Promise(function(resolve, reject){
const parsed = parseInt(str);
if (isNaN(parsed)) reject("значение не является числом")
else resolve(parsed);
})
.then(function(value){ console.log("Результат операции:", value);},
function(error){ console.log("Возникла ошибка:", error);});
}
generateNumber("23");
generateNumber("hello");
В данном случае для того, чтобы в промис можно было передать разные данные, он определен как возващаемый результат функции generateNumber(). То есть в данном случае консольный вывод будет следующим:
Результат операции: 23 Возникла ошибка: значение не является числом
Rule of Thumb
Whenever you have a doubt about how to do something with promises — think about the synchronous version.
try{
var result = myFn(param);
// business logic with result
} catch(e) {
//error handling logic
}
This, at least to me looks a lot cleaner than a callback with a first parameter that is sometimes null.
The promises way is almost always very similar to the synchronous version of the problem:
myFn(param).then(function(result){
// business logic with result
}).catch(function(e){
//error handling logic
});
Where myFn would look something like when working with callbacks:
var myFn = function(param){
return new Promise(function(resolve, reject){
var calc = doSomeCalculation(param);
if(calc === null) { // or some other way to detect error
reject(new Error("error with calculation"), null);
}
someAsyncOp(calcN,function(err, finalResult){
if(err) reject(err);
resolve(finalResult);
})
});
};
Working with callbacks/nodebacks
This is only something you should have to do when working with callbacks, when working with promises it is a lot simpler, and you can do:
var myFn = function(param){
var calc = doSomeCalculation(param);
...
return someAsyncOp(calcN); // returning a promise.
}
Moreover, when working inside promise chains, you get throw safety:
myFn(param).then(function(calcN){
// here, you throw to raise an error and return to resolve
// new Promise should be used only when starting a chain.
}).catch(function(err){
// handle error
}).then(function(){
// ready to go again, we're out of the catch
});
Note, some libraries like Bluebird , RSVP and Q offer syntactic sugar and automatic promisification of methods so you rarely have to use new Promise yourself.
Also, consider reading this and that to learn more about promise error handling.