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

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

// 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 не оставляет для этого места, каков рекомендуемый шаблон для обработки этого без использования библиотеки обещаний?

Ответ 1

Ответ Бенджамина предлагает отличную абстракцию для решения этой проблемы, но я надеялся на менее абстрактное решение. .catch способом решения этой проблемы является простой вызов .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 СУХОЙ, когда я должен сделать этот компромисс - именно поэтому я решил не использовать reflect.

Ответ 2

Конечно, вам просто нужно 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: "fulfilled" }},
                        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 === "fulfilled");
});

Ответ 3

Подобный ответ, но более идиоматический для 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>

Ответ 4

Существует предложение для функции, которая может выполнить это изначально, в ванильном Javascript: Promise.allSettled. В настоящее время он находится на стадии 4, и вполне вероятно, что он будет включен в официальную спецификацию. Это очень похоже на функцию reflect в этом другом ответе. Вот пример со страницы предложения. Раньше вы должны были сделать:

function reflect(promise) {
  return promise.then(
    (v) => {
      return { status: 'fulfilled', value: v };
    },
    (error) => {
      return { status: 'rejected', reason: error };
    }
  );
}

const promises = [ fetch('index.html'), fetch('https://does-not-exist/') ];
const results = await Promise.all(promises.map(reflect));
const successfulPromises = results.filter(p => p.status === 'fulfilled');

Если вместо этого использовать Promise.allSettled, вышеприведенное будет эквивалентно:

const promises = [ fetch('index.html'), fetch('https://does-not-exist/') ];
const results = await Promise.allSettled(promises);
const successfulPromises = results.filter(p => p.status === 'fulfilled');

Как только это станет частью спецификации и браузеры ее реализуют, вы сможете использовать ее в современных браузерах без каких-либо библиотек.

Он поставляется в Chrome 76, поэтому следующий фрагмент кода должен работать без проблем:

Promise.allSettled([
  Promise.resolve('a'),
  Promise.reject('b')
])
  .then(console.log);

Ответ 5

Мне очень нравится ответ Бенджамина и как он в основном превращает все promises в всегда-разрешающие, но иногда-с-error-as-a-result.:)
Здесь моя попытка по вашему запросу на всякий случай вы искали альтернативы. Этот метод просто рассматривает ошибки как допустимые результаты и кодируется аналогично 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);
  });
}

Ответ 6

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

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

Ответ 7

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

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 будет ожидать, что каждое Promise войдет в состояние resolved или rejected.

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

Ответ 8

Это должно соответствовать как 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,
        }))));
    };
}

Ответ 9

Ответ Бенджамина Грюнбаума, конечно, велик. Но я также вижу, что точка зрения Натана Хагена с уровнем абстракции кажется туманной. Короткие свойства объекта, такие как e & v тоже не помогают, но, конечно, это можно изменить.

В Javascript есть стандартный объект Error, называемый Error. В идеале вы всегда бросаете экземпляр/потомок этого. Преимущество в том, что вы можете сделать instanceof Error, и вы знаете, что что-то является ошибкой.

Таким образом, используя эту идею, вот мой взгляд на проблему.

В основном, перехватите ошибку, если ошибка не относится к типу Error, оберните ошибку внутри объекта Error. Полученный массив будет иметь либо разрешенные значения, либо объекты Error, которые вы можете проверить.

Экземпляр внутри функции catch, в случае, если вы используете какую-то внешнюю библиотеку, которая, возможно, reject("error") вместо reject(new Error("error")).

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

Еще одно преимущество этого - уничтожение массива - это просто.

const [value1, value2] = PromiseAllCatch(promises);
if (!(value1 instanceof Error)) console.log(value1);

Вместо

const [{v: value1, e: error1}, {v: value2, e: error2}] = Promise.all(reflect..
if (!error1) { console.log(value1); }

Вы можете утверждать, что проверка !error1 проще, чем instanceof, но вам также !error1 уничтожить оба v & e.

function PromiseAllCatch(promises) {
  return Promise.all(promises.map(async m => {
    try {
      return await m;
    } catch(e) {
      if (e instanceof Error) return e;
      return new Error(e);
    }
  }));
}


async function test() {
  const ret = await PromiseAllCatch([
    (async () => "this is fine")(),
    (async () => {throw new Error("oops")})(),
    (async () => "this is ok")(),
    (async () => {throw "Still an error";})(),
    (async () => new Error("resolved Error"))(),
  ]);
  console.log(ret);
  console.log(ret.map(r =>
    r instanceof Error ? "error" : "ok"
    ).join(" : ")); 
}

test();

Ответ 10

Я думаю, что следующее предлагает немного другой подход... сравните fn_fast_fail() с fn_slow_fail()... хотя последний не ошибается как таковой... вы можете проверить, является ли один или оба из a и b экземпляр Error и throw, который Error, если вы хотите, чтобы он достиг блока catch (например, if (b instanceof Error) { throw b; }). Смотрите jsfiddle.

var p1 = new Promise((resolve, reject) => { 
    setTimeout(() => resolve('p1_delayed_resolvement'), 2000); 
}); 

var p2 = new Promise((resolve, reject) => {
    reject(new Error('p2_immediate_rejection'));
});

var fn_fast_fail = async function () {
    try {
        var [a, b] = await Promise.all([p1, p2]);
        console.log(a); // "p1_delayed_resolvement"
        console.log(b); // "Error: p2_immediate_rejection"
    } catch (err) {
        console.log('ERROR:', err);
    }
}

var fn_slow_fail = async function () {
    try {
        var [a, b] = await Promise.all([
            p1.catch(error => { return error }),
            p2.catch(error => { return error })
        ]);
        console.log(a); // "p1_delayed_resolvement"
        console.log(b); // "Error: p2_immediate_rejection"
    } catch (err) {
        // we don't reach here unless you throw the error from the 'try' block
        console.log('ERROR:', err);
    }
}

fn_fast_fail(); // fails immediately
fn_slow_fail(); // waits for delayed promise to resolve

Ответ 11

Здесь мой обычай settledPromiseAll()

const settledPromiseAll = function(promisesArray) {
  var savedError;

  const saveFirstError = function(error) {
    if (!savedError) savedError = error;
  };
  const handleErrors = function(value) {
    return Promise.resolve(value).catch(saveFirstError);
  };
  const allSettled = Promise.all(promisesArray.map(handleErrors));

  return allSettled.then(function(resolvedPromises) {
    if (savedError) throw savedError;
    return resolvedPromises;
  });
};

По сравнению с Promise.all

  • Если все обещания разрешены, он работает точно так же, как стандартный.

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

Для смелых мы могли бы изменить Promise.all():

(function() {
  var stdAll = Promise.all;

  Promise.all = function(values, wait) {
    if(!wait)
      return stdAll.call(Promise, values);

    return settledPromiseAll(values);
  }
})();

ОСТОРОЖНЫЙ. В общем случае мы никогда не меняем встроенные модули, так как это может привести к поломке других несвязанных библиотек JS или столкнуться с будущими изменениями стандартов JS.

Мое settledPromiseall обратно совместимо с Promise.all и расширяет его функциональность.

Люди, которые разрабатывают стандарты - почему бы не включить это в новый стандарт Promise?

Ответ 12

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

const promise = arg => {
  return new Promise((resolve, reject) => {
      setTimeout(() => {
        try{
          if(arg != 2)
            return resolve({success: true, data: arg});
          else
            throw new Error(arg)
        }catch(e){
          return resolve({success: false, error: e, data: arg})
        }
      }, 1000);
  })
}

Promise.all([1,2,3,4,5].map(e => promise(e))).then(d => console.log(d))

Ответ 13

Я бы сделал:

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

Ответ 14

Вы можете выполнить свою логику последовательно через синхронный исполнитель nsynjs. Он будет останавливаться на каждом обещании, ждать разрешения/отклонения и либо назначать результат решения для свойства data, либо выдавать исключение (для обработки вам понадобится блок try/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>

Ответ 15

Я использую следующие коды с 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 будет ждать, пока все обещания закончат работу.

Ответ 16

Я знаю, что на этот вопрос есть много ответов, и я уверен, что должны (если не все) правильно. Однако мне было очень трудно понять логику/последовательность этих ответов.

Поэтому я посмотрел на Promise.all() реализацию в Promise.all() и попытался имитировать эту логику - за исключением того, что не прекратил выполнение, если одно Обещание не удалось.

  public promiseExecuteAll(promisesList: Promise<any>[] = []): Promise<{ data: any, isSuccess: boolean }[]>
  {
    let promise: Promise<{ data: any, isSuccess: boolean }[]>;

    if (promisesList.length)
    {
      const result: { data: any, isSuccess: boolean }[] = [];
      let count: number = 0;

      promise = new Promise<{ data: any, isSuccess: boolean }[]>((resolve, reject) =>
      {
        promisesList.forEach((currentPromise: Promise<any>, index: number) =>
        {
          currentPromise.then(
            (data) => // Success
            {
              result[index] = { data, isSuccess: true };
              if (promisesList.length <= ++count) { resolve(result); }
            },
            (data) => // Error
            {
              result[index] = { data, isSuccess: false };
              if (promisesList.length <= ++count) { resolve(result); }
            });
        });
      });
    }
    else
    {
      promise = Promise.resolve([]);
    }

    return promise;
  }

Объяснение:
- Зациклите входной список promisesList и выполните каждое Обещание.
- Независимо от того, было ли Обещание разрешено или отклонено: сохраните результат Обещания в массиве result соответствии с index. Сохраните также статус разрешения/отклонения (isSuccess).
- После выполнения всех обещаний верните одно обещание с результатами всех остальных.

Пример использования:

const p1 = Promise.resolve("OK");
const p2 = Promise.reject(new Error(":-("));
const p3 = Promise.resolve(1000);

promiseExecuteAll([p1, p2, p3]).then((data) => {
  data.forEach(value => console.log('${ value.isSuccess ? 'Resolve' : 'Reject' } >> ${ value.data }'));
});

/* Output: 
Resolve >> OK
Reject >> :-(
Resolve >> 1000
*/

Ответ 17

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

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

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