Комментарии 37
Promise.race (ES2015; замыкается на первом разрешенном промисе)
Promise.any (Stage 1; замыкается на первом удовлетворенном промисе)
не очень понятна разница между "удовлетворенным" и "разрешенным". Лучше было бы сказать "race — на первом хоть как-то завершенном", "any – на первом успешно завершенном"
Наверное, вы правы. Указал английские варианты. Разница между терминами хорошо описана в документе States and Fates, который являлся частью пропозала промисов для ES2015.
Просто не очень понятно, как можно использовать Promise.all() на практике с async-await, если ошибки там чреваты «висящими в воздухе» оставшимися промисами, продолжающими работать непредсказуемое время. И allSettled() тут не спасает от бойлерплейта.
В своем коде пришлось в свое время по этой причине выпилить все-все Promise.all() и заменить их на собственноручную joinSafe(), которая делает ровно это: ждет всех, но бросает первое. Насколько я понимаю, это поведение является также дефолтным в await genv() в Hack’е.
Я правильно понимаю, что ненавязчиво хотят ввести публичный статус промиса? Или я что-то пропустил и он давно есть?
С чего вы это взяли? Статус промиса не вводили ранее по той причине, что хотели сделать статус pending ненаблюдаемым. И новое api ничего в этом плане не меняет.
В самом промисе будет скрытое поле [[PromiseStatus]]
, но можно получить эти значения в then
:
var assert = require('assert');
var allSettled = require('promise.allsettled');
var resolved = Promise.resolve(42);
var rejected = Promise.reject(-1);
function prettyJSON(obj) {
console.log(JSON.stringify(obj, null, 2));
}
allSettled([resolved, rejected]).then(function (results) {
prettyJSON(results);
assert.deepEqual(results, [
{ status: 'fulfilled', value: 42 },
{ status: 'rejected', reason: -1 }
]);
});
Протестить этот код можно или через Ноду (npm i promise.allsettled
) или в каком-нибудь свежем Chrome Canary (заменив вызов allSettled
на Promise.allSettled
)
allSettled возвращает не массив промисов, а обертки promise+status. В Readme есть пример как добиться того же самого сегодняшними средствами
function reflect(promise) {
return promise.then(
(v) => {
return { status: 'fulfilled', value: v };
},
(error) => {
return { status: 'rejected', reason: error };
}
);
}
так что ничего нового нам из приватных свойств не открывают
В оригинале про Promise.all написана ерунда, ну и в переводе аналогично.
Promise.raсe: так как в JS нельзя результом промиса делать промис, то в штатной функции невозможно определить, какой конкретно промис завершился.
Обход:
/**
* Возвращаемый промис будет завершен, когда любой из последовательности промисов завершен.
* Возвращаемый промис всегда будет завершаться в состоянии resolved.
* Это справедливо, даже если первый завершенный промис находится в состоянии rejected.
*
* Поскольку в JavaScript результат промиса не может быть промисом, то
* результат возвращенного промиса — массив из одного элемента: первого завершенного промиса.
*/
Promise.whenAny = promises => new Promise((resolve, reject) => {
let result;
for (let promise of promises) {
if (result) break;
let func = () => { if (!result) { result = [promise]; resolve(result); } };
promise.then(func, func);
}
});
Promise.all: как выше было замечено, слишком рано вылетает на ошибке, поэтому не достигает функциональности Task.WhenAll()
Обход:
/**
* Аналог черновика Promise.allSettled(). Не полностью соответствует Task.WhenAll()
* Возвращаемый промис будет завершен, когда все из последовательности промисов будут завершены.
* Возвращаемый промис всегда будет завершаться в состоянии resolved, в отличие от Task.WhenAll()
* Это справедливо, даже если завершенные промисы будут находиться в состоянии rejected.
*
* @returns {Array} Массив объектов {value, reason, status: "fulfilled" или "rejected"}
* Свойства value или reason могут отсутствовать.
*/
if (!Promise.allSettled)
Promise.allSettled = promises => new Promise(async (resolve, reject) => {
let array = [];
for (let promise of promises) {
try {
let result = await promise;
array.push({ status: "fulfilled", value: result });
}
catch (ex) {
array.push({ status: "rejected", reason: ex });
}
}
resolve(array);
});
В сущности прямой эквивалент Task.WhenAll() уже не нужен. Мы можем перебрать исходную коллекцию промисов и оттуда забрать нужные результаты и ошибки.
Можно отметить, что свойства возвращаемого объекта в Promise.allSettled не соответствуют функции reflect() из npm promise-reflect.
Обход:
/**
* Получить информацию о промисе после того, как дождались его завершения.
* @param {Promise} promise Промис, для которого нужно получить информацию.
* @returns {Promise<{status:String,value:Any}|{status:String,reason:Error}>} Промис с объектом информации.
*/
Promise.reflect = promise => {
return promise.then(
(v) => {
return { status: 'fulfilled', value: v };
},
(error) => {
return { status: 'rejected', reason: error };
}
);
}
Соответственно, теперь уже будет можно по-человечески реализовать Task Asynchronous Pattern из:
devblogs.microsoft.com/pfxteam/processing-tasks-as-they-complete
codeblog.jonskeet.uk/2012/01/16/eduasync-part-19-ordering-by-completion-ahead-of-time
github.com/StephenCleary/AsyncEx/blob/master/src/Nito.AsyncEx.Tasks/TaskExtensions.cs
Ваша реализация whenAny содержит избыточную переменную result — она просто не нужна.
А ваша реализация allSettled оставит мусор в консоли. Лучше использовать способ из статьи (Promise.all(promises.map(...))
)
А например браузер Opera или NodeJS не оставляют мусор.
Получается, если бояться мусора, то невозможно использовать try..catch.
В общем и целом варианты с then() и try..catch эквивалентны. Допустим, в MS Edge мы будем использовать только then().
Тест в NodeJS:
/**
* Получить информацию о промисе после того, как дождались его завершения.
* @param {Promise} promise Промис, для которого нужно получить информацию.
* @returns {Promise<{status:String,value:Any}|{status:String,reason:Error}>} Промис с объектом информации.
*/
Promise.reflect = promise => {
return promise.then(
(v) => {
return { status: 'fulfilled', value: v };
},
(error) => {
return { status: 'rejected', reason: error };
}
);
}
if (!Promise.allSettled){
console.log("Не было Promise.allSettled");
Promise.allSettled = promises => new Promise(async (resolve, reject) => {
let array = [];
for (let promise of promises) {
array.push(await Promise.reflect(promise));
}
resolve(array);
});
// Promise.allSettled = promises => new Promise(async (resolve, reject) => {
// let array = [];
// for (let promise of promises) {
// try {
// let result = await promise;
// array.push({ status: "fulfilled", value: result });
// }
// catch (ex) {
// array.push({ status: "rejected", reason: ex });
// }
// }
// resolve(array);
// });
}
// 4) ОБРАБОТКА ИСКЛЮЧЕНИЙ.
async function ThrowNotImplementedExceptionAsync() {
throw new Error("Not implemented");
}
async function ThrowInvalidOperationExceptionAsync() {
throw new Error("Invalid operation");
}
/**
* Дождаться завершения всех промисов и отдельно перебрать исключения.
*/
async function ObserveAllExceptionsAsync() {
let task1 = ThrowNotImplementedExceptionAsync();
let task2 = ThrowInvalidOperationExceptionAsync();
let array = await Promise.allSettled([task1, task2]);
let allExceptions = array.filter(e => e.status === "rejected").map(e => e.reason);
for (let e of allExceptions)
console.log(e.message);
}
async function Method4Async() {
await ObserveAllExceptionsAsync();
}
(async function main() {
// 4) ОБРАБОТКА ИСКЛЮЧЕНИЙ.
await Method4Async();
})();
Вот вам другой тест:
Promise.allSettled([
new Promise(r => setTimeout(r, 1000, 1)),
Promise.reject(2),
])
В течении секунды в консоли Хрома наблюдается ошибка "Uncaught (in promise): 2".
Не было Promise.allSettled
(node:26108) UnhandledPromiseRejectionWarning: 2
(node:26108) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). (rejection id: 1)
(node:26108) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.
[ { status: 'fulfilled', value: 1 },
{ status: 'rejected', reason: 2 } ]
(node:26108) PromiseRejectionHandledWarning: Promise rejection was handled asynchronously (rejection id: 1)
Сначала матерится, потом увидел, что исключение обработано, и заткнулся )))
Это не дурдом, а полезная диагностика, помогающая отлавливать забытые ошибки. Но да, у нее есть один недостаток: на любой промис нужно подписываться сразу же, до того как он выполнится с ошибкой, иначе эта диагностика сработает ложноположительно.
В C# первая инструкция уже падает с ошибкой. Там не допускается запуск задач, завершающихся ошибкой вне try...catch или без ContinueWith().
const promises = [new Promise(r => setTimeout(r, 1000, 1)),
ThrowNotImplementedExceptionAsync()];
const results = await Promise.all(promises.map(Promise.reflect));
console.log(results);
А JavaScript непонятно почему допускает.
Там не допускается запуск задач, завершающихся ошибкой вне try...catch или без ContinueWith().
Не верю, вы что-то путаете. Там проверка идет в момент сборки задачи сборщиком мусора, и там как раз ваш алгоритм будет работать без проблем.
private static Task<int> ThrowNotImplementedExceptionAsync()
{
throw new Exception("Ошибка2");
}
private static async Task<int> DelayAndReturnAsync(int val)
{
await Task.Delay(val * 1000);
return val;
}
private static async Task TestTryCatchAsync2()
{
var promises = new Task<int>[] {
DelayAndReturnAsync(1), // Задача без ошибки
ThrowNotImplementedExceptionAsync() // Задача с ошибкой
};
async Task<dynamic> reflect(Task<int> task)
{
try
{
var result = await task;
return new { value = result, status = "rejected" };
}
catch (Exception e)
{
return new { reason = e, status = "rejected" };
}
}
var results = await Task.WhenAll(promises.Select(reflect).ToArray());
Console.WriteLine(results[0].value);
}
Падение именно при исполнении инструкции объявления массива задач.
Достаточно убрать задачу с ошибкой, и всё выполнится.
if (!Promise.allSettled) {
console.log("Не было Promise.allSettled");
Promise.allSettled = promises => new Promise((resolve, reject) => {
let inputTasks = Array.from(promises);
let array = [],
count = 0,
len = inputTasks.length;
for (let i = 0; i < len; ++i) {
inputTasks[i].then(
value => {
array[i] = { status: "fulfilled", value: value };
if (++count === len) resolve(array);
},
reason => {
array[i] = { status: "rejected", reason: reason };
if (++count === len) resolve(array);
}
);
}
});
}
/**
* Возвращаемый промис будет завершен, когда любой из последовательности промисов завершен.
* Возвращаемый промис всегда будет завершаться в состоянии resolved.
* Это справедливо, даже если первый завершенный промис находится в состоянии rejected.
*
* Поскольку в JavaScript результат промиса не может быть промисом, то
* результат возвращенного промиса — массив из одного элемента: первого завершенного промиса.
*/
Promise.whenAny = promises => new Promise((resolve, reject) => {
for (let promise of promises) {
let func = () => { resolve([promise]); };
promise.then(func, func);
}
});
/*
* Task.Delay()
*/
Promise.delay = ms => new Promise(resolve => { setTimeout(resolve, ms); });
// ОБРАБОТКА ИСКЛЮЧЕНИЙ.
async function ThrowNotImplementedExceptionAsync() {
throw new Error("Not implemented");
}
async function ThrowInvalidOperationExceptionAsync() {
throw new Error("Invalid operation");
}
/**
* Тестовая функция. Подождать определенное время и вернуть результат.
* @param {Number} val Возвращаемое число, одновременно являющееся задержкой в секундах.
* @returns {Promise<Number>} Возвращаемое число, взятое из параметра функции.
*/
async function delayAndReturnAsync(/*int*/ val) {
await Promise.delay(val * 1000);
return val;
}
/**
* Дождаться завершения всех промисов и отдельно перебрать исключения.
*/
async function ObserveAllExceptionsAsync() {
let task1 = ThrowNotImplementedExceptionAsync();
let task2 = ThrowInvalidOperationExceptionAsync();
let array = await Promise.allSettled([task1, task2]);
let allExceptions = array.filter(e => e.status === "rejected").map(e => e.reason);
for (let e of allExceptions)
console.log(e.message);
}
async function Method4Async() {
await ObserveAllExceptionsAsync();
}
(async function main() {
await Method4Async();
console.log(await Promise.allSettled([
new Promise(r => setTimeout(r, 1000, 1)),
Promise.reject(2),
]));
})();
Лучше все-таки не навешивать свои кастомные методы на нативный объект Promise. Особенно, подкладывать реализацию allSettled, которая отличается от стандарта.
"ущербность" нынешних методов – это ваше личное мнение, а если какой-то код захочет использовать стандартный метод и не получит ожидаемый результат, будет очень плохо.
Лучше создать npm-модуль, например promises-with-blackjack (имя свободно, если что) и положить свои реализации туда, не перекрывая нативные.
Ну не все так плохо. Его реализация allSettled стандарту соответствует, отличия в поведении наблюдаемы только отладчиком.
Вот с reflect и whenAny всё уже хуже, но их вроде и не планируют добавлять в стандарт, так что конфликт возможен лишь с другим таким же "патчером"...
Действительно, более-менее похожая реализация.
В отладчике мы увидем сообщение об unhandledRejection, я так понимаю? Это может выстрелить в ногу, потому что в Node.js 12 добавили новый флаг --unhandled-rejections=strict
, который может завершить процесс в этой ситуации.
(комментарий был удален)
А вот мне кажется, что оба новых метода Promise.allSettled
/ Promise.any
принесут больше проблем, чем пользы.
Promise.allSettled
добавляет новый уровень абстракции, заворачивая результаты в объект вида{status, reason}
.Promise.any
декларирует новый тип ошибкиAggregateError
.- И оба метода фактически позволяют игнорировать ошибки в коде, что рано или поздно аукнется. Лучше бы это оставалось в области библиотек, но не core Promise API.
Буквально сегодня выложил на эту тему статью.
Promise.allSettled
добавляет новый уровень абстракции, заворачивая результаты в объект вида{status, reason}
А в чем проблема? И почему простейшая структура данных называется "уровнем абстракции"?
Promise.any декларирует новый тип ошибки AggregateError.
И в чем проблема?
И оба метода фактически позволяют игнорировать ошибки в коде, что рано или поздно аукнется.
И оба метода следует применять в случае, когда ошибка должна быть проигнорирована при соответствующих условиях.
Лучше бы это оставалось в области библиотек, но не core Promise API.
Почему лучше?
новый тип ошибки AggregateError
Это старый тип ошибки в C#. Но с ним много возни в коде. Стивен Клири, автор книги «Concurrency in C# Cookbook», не любит этот массив ошибок.
В принципе метод Promise.allSettled() был бы неплохим способом не корячиться с этим массивом ошибок.
Но опять же есть желающие и полностью эмулировать Task.WhenAll().
По вашей статье:
In how many projects did you use the pattern make several parallel requests to identical endpoints for the same data?
Не обязательно это должны быть одинаковые конечные точки. И не обязательно там должны быть одинаковые данные.
Вы же сами писали в примере всякие .then(() => 'a')
и .then(() => 'b')
— это разве одинаковые данные?
I agree sometimes it may be useful. But how often?
Вопрос не в том, насколько часто — а в том, что делать когда эта возможность нужна. А делать нечего, текущее api не предоставляет нужной функции понятным путём (и нет, вариант с reverse непонятен!).
I think the core API should expose all errors.
В таком случае надо запретить метод catch — он ведь тоже относится к core API и тоже позволяет проигнорировать ошибку.
Promise.allSettled