Подождите, пока все обещания ES6 не будут выполнены, даже отклоненные обещания


допустим у меня есть набор обещаний, которые делают сетевые запросы, один из которых будет выполнена:

// http://does-not-exist will throw a TypeError
var arr = [ fetch('index.html'), fetch('http://does-not-exist') ]

Promise.all(arr)
  .then(res => console.log('success', res))
  .catch(err => console.log('error', err)) // This is executed   

допустим, я хочу подождать, пока все они не закончатся, независимо от того, если один из них не удалось. Может быть сетевая ошибка для ресурса, без которого я могу жить, но который, если я могу получить, я хочу, прежде чем продолжить. Я хочу обрабатывать сетевые сбои изящно.

С Promises.all не оставляет места для этого, чем рекомендуется шаблон для обработки этого, без использования библиотеки обещаний?

12 234

12 ответов:

конечно, вам просто нужно reflect:

const reflect = p => p.then(v => ({v, status: "fulfilled" }),
                            e => ({e, status: "rejected" }));

reflect(promise).then((v => {
    console.log(v.status);
});

или с ES5:

function reflect(promise){
    return promise.then(function(v){ return {v:v, status: "resolved" }},
                        function(e){ return {e:e, status: "rejected" }});
}


reflect(promise).then(function(v){
    console.log(v.status);
});

или в вашем примере:

var arr = [ fetch('index.html'), fetch('http://does-not-exist') ]

Promise.all(arr.map(reflect)).then(function(results){
    var success = results.filter(x => x.status === "resolved");
});

аналогичный ответ, но более идиоматичный для ES6, возможно:

const a = Promise.resolve(1);
const b = Promise.reject(new Error(2));
const c = Promise.resolve(3);

Promise.all([a, b, c].map(p => p.catch(e => e)))
  .then(results => console.log(results)) // 1,Error: 2,3
  .catch(e => console.log(e));


const console = { log: msg => div.innerHTML += msg + "<br>"};
<div id="div"></div>

в зависимости от типа(ов) возвращаемых значений, ошибки часто можно отличить достаточно легко (например, использование undefined для "не волнует", typeof для равнины non-object значения, result.message,result.toString().startsWith("Error:") etc.)

ответ Бенджамина предлагает отличную абстракцию для решения этой проблемы, но я надеялся на менее абстрактное решение. Явный способ решить эту проблему-просто вызвать .catch на внутренних обещаниях и возвращает ошибку из их обратного вызова.

let a = new Promise((res, rej) => res('Resolved!')),
    b = new Promise((res, rej) => rej('Rejected!')),
    c = a.catch(e => { console.log('"a" failed.'); return e; }),
    d = b.catch(e => { console.log('"b" failed.'); return e; });

Promise.all([c, d])
  .then((result => console.log('Then', result)) // Then ["Resolved!", "Rejected!"]
  .catch(err => console.log('Catch', err));

Promise.all([a.catch(e => e), b.catch(e => e)])
  .then(result => console.log('Then', result)) // Then ["Resolved!", "Rejected!"]
  .catch(err => console.log('Catch', err));

сделав этот шаг дальше, вы можете написать универсальный обработчик catch, который выглядит так:

const catchHandler = error => ({ payload: error, resolved: false });

затем вы можете сделать

> Promise.all([a, b].map(promise => promise.catch(catchHandler))
    .then(results => console.log(results))
    .catch(() => console.log('Promise.all failed'))
< [ 'Resolved!',  { payload: Promise, resolved: false } ]

проблема с этим что пойманные значения будут иметь другой интерфейс, чем не пойманные значения, поэтому для очистки этого вы можете сделать что-то вроде:

const successHandler = result => ({ payload: result, resolved: true });

так что теперь вы можете сделать это:

> Promise.all([a, b].map(result => result.then(successHandler).catch(catchHandler))
    .then(results => console.log(results.filter(result => result.resolved))
    .catch(() => console.log('Promise.all failed'))
< [ 'Resolved!' ]

затем, чтобы сохранить его сухим, вы получите ответ Вениамина:

const reflect = promise => promise
  .then(successHandler)
  .catch(catchHander)

где это сейчас выглядит

> Promise.all([a, b].map(result => result.then(successHandler).catch(catchHandler))
    .then(results => console.log(results.filter(result => result.resolved))
    .catch(() => console.log('Promise.all failed'))
< [ 'Resolved!' ]

преимущества второго решения в том, что его абстрагируют и сушат. Недостатком является то, что у вас больше кода, и вы должны помнить, чтобы отразить все ваши обещания сделать вещи последовательными.

я бы охарактеризовал свое решение как явное и поцеловать, но на самом деле менее надежным. Интерфейс не гарантирует, что вы точно знаете, удалось ли обещание или не удалось.

например, у вас может быть это:

const a = Promise.resolve(new Error('Not beaking, just bad'));
const b = Promise.reject(new Error('This actually didnt work'));

это не будет пойман a.catch, так что

> Promise.all([a, b].map(promise => promise.catch(e => e))
    .then(results => console.log(results))
< [ Error, Error ]

нет никакого способа, чтобы сказать, какой из них был смертельным и который был не. Если это важно, то вы будете хотеть чтобы обеспечить и интерфейс, который отслеживает, был ли он успешным или нет (который reflect делает).

если вы просто хотите обрабатывать ошибки изящно, то вы можете просто рассматривать ошибки как неопределенные значения:

> Promise.all([a.catch(() => undefined), b.catch(() => undefined)])
    .then((results) => console.log('Known values: ', results.filter(x => typeof x !== 'undefined')))
< [ 'Resolved!' ]

в моем случае мне не нужно знать ошибку или как она не удалась-мне просто важно, есть ли у меня значение или нет. Я позволю функции, которая генерирует обещание беспокоиться о регистрации конкретной ошибки.

const apiMethod = () => fetch()
  .catch(error => {
    console.log(error.message);
    throw error;
  });

таким образом, остальная часть приложение может игнорировать свою ошибку, если оно хочет, и рассматривать ее как неопределенное значение, если оно хочет.

я хочу, чтобы мои функции высокого уровня безопасно отказывали и не беспокоились о деталях, почему его зависимости не удались, и я также предпочитаю KISS To DRY, когда мне нужно сделать этот компромисс-и именно поэтому я решил не использовать reflect.

мне очень нравится ответ Бенджамина, и как он в основном превращает все обещания в Всегда разрешающие, но иногда с ошибкой в результате. :)
Вот моя попытка по вашему запросу на случай, если вы искали альтернативы. Этот метод просто обрабатывает ошибки как допустимые результаты и кодируется аналогично Promise.all в противном случае:

Promise.settle = function(promises) {
  var results = [];
  var done = promises.length;

  return new Promise(function(resolve) {
    function tryResolve(i, v) {
      results[i] = v;
      done = done - 1;
      if (done == 0)
        resolve(results);
    }

    for (var i=0; i<promises.length; i++)
      promises[i].then(tryResolve.bind(null, i), tryResolve.bind(null, i));
    if (done == 0)
      resolve(results);
  });
}

у меня была такая же проблема и я решил ее следующим образом:

const fetch = (url) => {
  return node-fetch(url)
    .then(result => result.json())
    .catch((e) => {
      return new Promise((resolve) => setTimeout(() => resolve(fetch(url)), timeout));
    });
};

tasks = [fetch(url1), fetch(url2) ....];

Promise.all(tasks).then(......)

в этом случае Promise.all будет ждать, пока каждое обещание вступит в resolved или rejected государство.

и имея это решение мы "остановка catch выполнение " неблокирующим способом. На самом деле, мы ничего не останавливаем, мы просто возвращаем обратно Promise в состоянии ожидания, которое возвращает другой Promise когда он будет разрешен после тайм-аута.

var err;
Promise.all([
    promiseOne().catch(function(error) { err = error;}),
    promiseTwo().catch(function(error) { err = error;})
]).then(function() {
    if (err) {
        throw err;
    }
});

The Promise.all проглотит любое отклоненное обещание и сохранит ошибку в переменной, поэтому она вернется, когда все обещания будут разрешены. Затем вы можете повторно выбросить ошибку или сделать что угодно. Таким образом, я думаю, вы получите последний отказ вместо первого.

Это должно быть согласовано с как Q делает это:

if(!Promise.allSettled) {
    Promise.allSettled = function (promises) {
        return Promise.all(promises.map(p => Promise.resolve(p).then(v => ({
            state: 'fulfilled',
            value: v,
        }), r => ({
            state: 'rejected',
            reason: r,
        }))));
    };
}

вы можете выполнить свою логику последовательно через синхронный исполнитель nsynjs. Он остановится на каждом обещании, дождется разрешения / отклонения и либо назначит результат resolve data свойства, или бросить исключение (для обработки, что вам нужно попробовать/catch блок). Вот пример:

function synchronousCode() {
    function myFetch(url) {
        try {
            return window.fetch(url).data;
        }
        catch (e) {
            return {status: 'failed:'+e};
        };
    };
    var arr=[
        myFetch("https://ajax.googleapis.com/ajax/libs/jquery/2.0.0/jquery.min.js"),
        myFetch("https://ajax.googleapis.com/ajax/libs/jquery/2.0.0/NONEXISTANT.js"),
        myFetch("https://ajax.NONEXISTANT123.com/ajax/libs/jquery/2.0.0/NONEXISTANT.js")
    ];
    
    console.log('array is ready:',arr[0].status,arr[1].status,arr[2].status);
};

nsynjs.run(synchronousCode,{},function(){
    console.log('done');
});
<script src="https://rawgit.com/amaksr/nsynjs/master/nsynjs.js"></script>

Я только что написал custom

Я бы сделал:

var err = [fetch('index.html').then((success) => { return Promise.resolve(success); }).catch((e) => { return Promise.resolve(e); }),
fetch('http://does-not-exist').then((success) => { return Promise.resolve(success); }).catch((e) => { return Promise.resolve(e); })];

Promise.all(err)
.then(function (res) { console.log('success', res) })
.catch(function (err) { console.log('error', err) }) //never executed

Я использую следующие коды с ES5.

Promise.wait = function(promiseQueue){
    if( !Array.isArray(promiseQueue) ){
        return Promise.reject('Given parameter is not an array!');
    }

    if( promiseQueue.length === 0 ){
        return Promise.resolve([]);
    }

    return new Promise((resolve, reject) =>{
        let _pQueue=[], _rQueue=[], _readyCount=false;
        promiseQueue.forEach((_promise, idx) =>{
            // Create a status info object
            _rQueue.push({rejected:false, seq:idx, result:null});
            _pQueue.push(Promise.resolve(_promise));
        });

        _pQueue.forEach((_promise, idx)=>{
            let item = _rQueue[idx];
            _promise.then(
                (result)=>{
                    item.resolved = true;
                    item.result = result;
                },
                (error)=>{
                    item.resolved = false;
                    item.result = error;
                }
            ).then(()=>{
                _readyCount++;

                if ( _rQueue.length === _readyCount ) {
                    let result = true;
                    _rQueue.forEach((item)=>{result=result&&item.resolved;});
                    (result?resolve:reject)(_rQueue);
                }
            });
        });
    });
};

подпись использования так же, как Promise.all. Главное отличие в том, что Promise.wait будет ждать всех обещаний, чтобы закончить свою работу.

Я не знаю, какую библиотеку обещаний вы используете, но у большинства есть что-то вроде allSettled.

Edit: Ok поскольку вы хотите использовать обычный ES6 без внешних библиотек, такого метода нет.

другими словами: вы должны зациклиться на своих обещаниях вручную и решить новый в сочетании обещайте, как только все обещания будут выполнены.