Pull to refresh

Comments 37

Promise.race (ES2015; замыкается на первом разрешенном промисе)
Promise.any (Stage 1; замыкается на первом удовлетворенном промисе)

не очень понятна разница между "удовлетворенным" и "разрешенным". Лучше было бы сказать "race — на первом хоть как-то завершенном", "any – на первом успешно завершенном"

Наверное, вы правы. Указал английские варианты. Разница между терминами хорошо описана в документе States and Fates, который являлся частью пропозала промисов для ES2015.

А метода, который ожидает разрешения всех (как и allSettled), но потом (после окончания ожидания) выбрасывает первое исключение из случившихся, пока не завезли?

Просто не очень понятно, как можно использовать Promise.all() на практике с async-await, если ошибки там чреваты «висящими в воздухе» оставшимися промисами, продолжающими работать непредсказуемое время. И allSettled() тут не спасает от бойлерплейта.

В своем коде пришлось в свое время по этой причине выпилить все-все Promise.all() и заменить их на собственноручную joinSafe(), которая делает ровно это: ждет всех, но бросает первое. Насколько я понимаю, это поведение является также дефолтным в await genv() в Hack’е.

Кажется, не завезли: единстенный кто ждёт всех — allSettled. С другой стороны, теперь можно проапдейтить ваш бойлерплейт :)

UFO just landed and posted this here

Я правильно понимаю, что ненавязчиво хотят ввести публичный статус промиса? Или я что-то пропустил и он давно есть?

С чего вы это взяли? Статус промиса не вводили ранее по той причине, что хотели сделать статус pending ненаблюдаемым. И новое api ничего в этом плане не меняет.

Из кода p.status === 'fulfilled'

В самом промисе будет скрытое поле [[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.allSettled([fetch(...)])[0] по сути будет полунативной обёрткой.

Ну что же, будем ждать Promise.settled() который будет делать то же самое, только для одного :)

В оригинале про Promise.all написана ерунда, ну и в переводе аналогично.

В чём эта ерунда заключается?

Спасибо, но я знаю эту ссылку. Лучше расскажите какое именно отличие вы нашли.

В ES2015 оба эквивалента C#-комбинаторов ущербные.
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(...)))

Мусор в консоли при использовании try..catch оставляет браузер MS Edge.
А например браузер 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".

Дурдом на выезде. NodeJS пишет:
Не было 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);
}

Падение именно при исполнении инструкции объявления массива задач.
Достаточно убрать задачу с ошибкой, и всё выполнится.
Виноват. Всё правильно. Я async забыл дописать. Всё работает хорошо и с ошибкой, как надо:
private static async Task<int> ThrowNotImplementedExceptionAsync()
{
    throw new Exception("Ошибка2");
}
С учётом ваших замечаний. Прошу потестить.
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 и тоже позволяет проигнорировать ошибку.

Sign up to leave a comment.

Articles