web-dev-qa-db-ja.com

ES6のすべての約束が完了するまで、さらに拒否された約束まで待つ

ネットワーク要求をしている約束のセットがあるとしましょう、そのうちの1つは失敗します:

// 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 にはこれ以上の余地がないので、promiseライブラリを使用せずにこれを処理するための推奨パターンは何ですか?

302
Nathan Hagen

ベンジャミンの答えはこの問題を解決するための素晴らしい抽象化を提供しますが、私は抽象化されていない解決策を望んでいました。この問題を解決する明示的な方法は、単に内部約束で.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!' ]

それからそれをDRYに保つために、あなたはベンジャミンの答えにたどり着きます:

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!' ]

2番目のソリューションの利点は、その抽象化とDRYです。欠点は、より多くのコードがあることです。そして、物事の一貫性を保つために、すべての約束を反映することを忘れないでください。

私の解決策を明示的なものとKISSとして特徴付けますが、確かにそれほど堅牢ではありません。インターフェースは、約束が成功したか失敗したかを正確に知っていることを保証するものではありません。

たとえば、これがあります。

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!' ]

私の場合は、エラーや失敗の仕方を知る必要はありません - 私は価値があるかどうかを気にするだけです。 promiseを生成する関数に、特定のエラーをログに記録することを心配させます。

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

そうすれば、アプリケーションの他の部分は必要に応じてエラーを無視し、必要に応じて未定義の値として扱うことができます。

私は私の高水準関数が安全に失敗し、その依存関係が失敗した理由の詳細について心配しないでほしいと思います、そしてまた私がKISSからDRYを選ぶことを好みます私がreflectを使わないことを選んだのは、結局のところです。

57
Nathan Hagen

もちろん、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");
});
242

同様の答えですが、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>

返される値の種類によっては、エラーを簡単に区別できることがよくあります(たとえば、 "don't care"にはundefined、単純な非オブジェクト値にはtypeofresult.messageresult.toString().startsWith("Error:")など)。

200
jib

私はBenjaminの答えがとても気に入っています。そして、Benjaminが基本的にすべての約束を常に解決しますが、結果としてエラーを伴うことがあるものに変えるのはどうしてですか。 :)
あなたが代替案を探していた場合に備えて、これがあなたの要求に対する私の試みです。このメソッドは単にエラーを有効な結果として扱い、それ以外の場合は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);
  });
}
9
Kuba Wyrostek
var err;
Promise.all([
    promiseOne().catch(function(error) { err = error;}),
    promiseTwo().catch(function(error) { err = error;})
]).then(function() {
    if (err) {
        throw err;
    }
});

Promise.allは拒否された約束を飲み込み、エラーに変数を格納するので、すべての約束が解決されたときに戻ります。それからあなたはエラーを捨てるか、あるいは何でもすることができます。このようにして、私はあなたが最初の拒絶の代わりに最後の拒絶を取り除くだろうと思います。

4
martin770

これをネイティブに達成できる関数のための 提案 がVanilla Javascriptにあります:Promise.allSettled。現在はステージ3にあり、正式な仕様にする可能性が非常に高いです。これは この別の回答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');

これが仕様の一部になり、ブラウザがそれを実装するようになると、最新のブラウザでライブラリを使用できなくなります ライブラリなしで .

4

私は同じ問題を抱えており、以下の方法でそれを解決しました:

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を返すだけです。

4
user1016265

Benjamin Gruenbaumの答えはもちろん素晴らしいです。しかし、抽象化のレベルに対するNathan Hagenの見方が曖昧に見えたこともわかります。 e & vのような短いオブジェクトプロパティを持っていても役に立ちませんが、もちろんそれは変更される可能性があります。

JavascriptにはErrorという標準のErrorオブジェクトがあります。理想的には、あなたはいつもこれのインスタンス/子孫を投げます。利点はinstanceof Errorを実行できることと、何かがエラーであることを知っていることです。

それで、この考えを使用して、ここで私は問題について考えます。

基本的にエラーをキャッチします。エラーの種類がErrorではない場合は、エラーをErrorオブジェクト内にラップします。結果の配列には、解決された値、またはチェック可能なErrorオブジェクトがあります。

Catchの内側のinstanceofは、あなたがreject("error")の代わりにreject(new Error("error"))をしたかもしれない何らかの外部ライブラリを使う場合のためのものです。

もちろん、エラーを解決することを約束することもできますが、その場合は、最後の例に示すように、エラーとして扱うのが最も理にかなっているでしょう。

これを行うもう1つの利点は、配列の破壊が簡単になることです。

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よりも単純だが、両方の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();
2
Keith

これは と一致しているべきです - 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,
        }))));
    };
}
2
mpen

fn_fast_fail()fn_slow_fail()...を比較すると、以下のように少し異なるアプローチが提供されると思います。後者は失敗しません... abの一方または両方を確認できます。 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
0
drmrbrewer

これが私のカスタム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との比較

  • すべての約束が解決されれば、それはまさに標準的なものとして機能します。

  • 複数の約束のうちの1つが拒​​否された場合、最初のものが標準のものとほぼ同じように拒否されたものを返しますが、すべての約束が解決または拒否されるのを待ちます。

勇敢な人には、Promise.all()を変更することができます。

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

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

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

_注意_ 。他の無関係なJSライブラリを壊したり、将来のJS標準の変更と衝突する可能性があるため、一般的には組み込みを変更することはありません。

私のsettledPromiseallPromise.allと下位互換性があり、その機能を拡張します。

標準を開発している人々 - なぜこれを新しいPromise標準に含めないのですか?

0
Edward

同期エグゼキュータ nsynjs を介してロジックを順次実行できます。それは各約束を一時停止し、解決/却下を待ち、resolveの結果を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>
0
amaksr

私はこの質問にたくさんの答えがあることを知っています、そして私は(すべてではないにしても)正しいと確信しています。しかし、これらの答えの論理や流れを理解するのは私にとって非常に困難でした。

そこで私はPromise.all()の元の実装を見て、その論理を真似ようとしました - 1つのPromiseが失敗しても実行を停止しないことを除いて。

public promiseExecuteAll(promisesList: Promise<any>[]): Promise<{ data: any, isSuccess: boolean }[]>
{
  const result: { data: any, isSuccess: boolean }[] = [];
  let count: number = 0;

  const 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); }
        });
    });
  });

  return promise;
}

説明:
- 入力promisesListをループして各Promiseを実行します。
- Promiseが解決または却下されたかどうかに関係なく、resultに従って、Promiseの結果をindex配列に保存します。解決/却下ステータス(isSuccess)も保存します。
- すべての約束が完了したら、他のすべての約束と共に1つの約束を返します。

使用例

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
*/
0
Gil Epshtain