1. ホーム
  2. ジャバスクリプト

[解決済み】一部の約束が拒否されても、すべての約束が完了するまで待つ

2022-04-18 11:07:52

質問

のセットがあるとします。 Promise がネットワークリクエストをしていて、そのうちの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   

例えば、1つが失敗しても、これらすべてが終了するまで待ちたいとします。ネットワークエラーが発生し、そのリソースがなくても生きていけるが、もし手に入るなら先に手に入れたいリソースがあるかもしれない。私はネットワーク障害を優雅に扱いたいのです。

以来 Promise.all はこのための場所を残しません、約束ライブラリを使用せずに、これを処理するための推奨されるパターンは何ですか?

どのように解決するのですか?

Benjaminの回答は、この問題を解決するための素晴らしい抽象化を提供していますが、私はより抽象化されていない解決策を望んでいました。この問題を解決するための明示的な方法は、単に .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));


さらに一歩進んで、次のような一般的なキャッチ・ハンドラを書くこともできます。

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を保つために、Benjaminの答えにたどり着きます。

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

私の場合、エラーの内容や失敗の原因を知る必要はなく、値があるかどうかだけが気になります。具体的なエラーのログは、プロミスを生成する関数に任せます。

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

そうすれば、アプリケーションの他の部分はそのエラーを無視してもいいし、未定義の値として扱ってもいいのです。

私は高レベルの関数が安全に失敗し、依存関係が失敗した理由の詳細を気にしないようにしたいのですが、そのトレードオフをしなければならないときはDRYよりもKISSを好みます。 reflect .