web-dev-qa-db-ja.com

Promiseチェーンでエラーを適切に処理する方法は?

Promiseを返す3つの非同期タスク、ABCがあるとします。それらを一緒に連鎖させたい(つまり、わかりやすくするために、Aによって返された値を取得し、Bを呼び出して)、それぞれについてエラーを正しく処理したい、および最初の失敗で発生します。現在、これを行うには2つの方法があります。

A
.then(passA)
.then(B)
.then(passB)
.then(C)
.then(passC)
.catch(failAll)

ここで、passX関数は、Xの呼び出しの成功のeachを処理します。しかし、failAll関数では、ABCのエラーのallを処理する必要があります。これらは特に複雑で読みにくい場合があります。 3つの非同期タスクよりも。したがって、他の方法ではこれを考慮します。

A
.then(passA, failA)
.then(B)
.then(passB, failB)
.then(C)
.then(passC, failC)
.catch(failAll)

ここでは、元のfailAllのロジックをfailAfailB、およびfailCに分離しました。これは、すべてのエラーがソースのすぐ隣で処理されるため、シンプルで読みやすいようです。 しかし、これは私が望むことをしません。

Aが失敗(拒否)した場合、failABの呼び出しに進まないため、例外をスローするか、rejectを呼び出す必要があります。しかし、これらは両方ともfailBfailCにキャッチされます。つまり、failBfailCは、おそらく状態を維持することによって、すでに失敗したかどうかすなわち変数)。

さらに、非同期タスクが増えると、failAll関数のサイズが大きくなるか(way 1)、またはfailX関数がさらに呼び出されるようになります(way 2)。これは私の質問に私をもたらします:

これを行うためのより良い方法はありますか?

考慮事項:thenの例外は拒否メソッドによって処理されるため、Promise.throw実際にチェーンを切り離す方法?

重複の可能性あり 。ハンドラー内にスコープを追加する回答があります。関数の線形連鎖を尊重し、関数を渡す関数を渡す関数を渡さないという約束はありませんか?

17
David Song

いくつかのオプションがあります。まず、あなたの要件を掘り下げることができるかどうか見てみましょう。

  1. エラーが発生した場所の近くでエラーを処理するため、考えられるすべてのエラーをソートして対処方法を確認する必要がある1つのエラーハンドラーがありません。

  2. 1つのプロミスが失敗した場合、チェーンの残りを中止する機能が必要です。

1つの可能性は次のとおりです。

_A().then(passA).catch(failA).then(val => {
    return B(val).then(passB).catch(failB);
}).then(val => {
    return C(val).then(passC).catch(failC);
}).then(finalVal => {
    // chain done successfully here
}).catch(err => {
    // some error aborted the chain, may or may not need handling here
    // as error may have already been handled by earlier catch
});
_

次に、各failAfailBfailCで、そのステップの特定のエラーを取得します。チェーンを中止する場合は、関数が戻る前に再スローします。チェーンを継続する場合は、通常の値を返すだけです。


上記のコードは、このように書くこともできます(passBまたはpassCが拒否されたプロミスをスローまたは返す場合の動作は少し異なります)。

_A().then(passA, failA).then(val => {
    return B(val).then(passB, failB);
}).then(val => {
    return C(val).then(passC, failC);
}).then(finalVal => {
    // chain done successfully here
}).catch(err => {
    // some error aborted the chain, may or may not need handling here
    // as error may have already been handled by earlier catch
});
_

これらは完全に反復的であるため、任意の長さのシーケンスに対してテーブル駆動型にすることもできます。

_function runSequence(data) {
    return data.reduce((p, item) => {
        return p.then(item[0]).then(item[1]).catch(item[2]);
    }, Promise.resolve());
}

let fns = [
    [A, passA, failA],
    [B, passB, failB],
    [C, passC, failC]
];

runSequence(fns).then(finalVal => {
    // whole sequence finished
}).catch(err => {
    // sequence aborted with an error
});
_

たくさんの約束をつなぐもう一つの便利な点は、リジェクトエラーごとに一意のErrorクラスを作成すると、最後の.catch()ハンドラーでinstanceofを使用してエラーの種類を簡単に切り替えることができることです。中断されたチェーンの原因となったステップを知る必要がある場合。 Bluebirdなどのライブラリは、特定のタイプのエラーのみを捕捉する.catch()を作成するための特定の.catch()セマンティクスを提供します(try/catchが行う方法など)。 Bluebirdがこれをどのように行うかは、ここで確認できます: http://bluebirdjs.com/docs/api/catch.html 。 (上記の例のように)独自のプロミス拒否で各エラーを処理する場合、最後の.catch()ステップでエラーの原因となったステップをまだ知る必要がない限り、これは不要です。 。

10
jfriend00

私がお勧めする2つの方法があります(これで何を達成しようとしているかによって異なります):

はい、Promiseチェーン内のすべてのエラーを1つのキャッチで処理する必要があります。

どれが失敗したかを知る必要がある場合は、次のような一意のメッセージまたは値を使用してプロミスを拒否できます。

A
.then(a => {
  if(!pass) return Promise.reject('A failed');
  ...
})
.then(b => {
  if(!pass) return Promise.reject('B failed');
  ...
})
.catch(err => {
  // handle the error
});

または、.then内で他のプロミスを返すことができます

A
.then(a => {
  return B; // B is a different promise
})
.then(b => {
  return C; // C is another promise
})
.then(c => {
  // all promises were resolved
  console.log("Success!") 
})
.catch(err => {
  // handle the error
  handleError(err)
});

これらの約束のそれぞれで、何らかのエラーメッセージが必要になるため、どのエラーが失敗したかがわかります。

そして、これらは矢印関数なので、ブレースを削除できます!約束が大好きなもう一つの理由

A
.then(a => B)
.then(b => C)
.then(c => console.log("Success!"))
.catch(err => handleError(err));
5
AJ Funk

promise-chainの分岐 が可能ですが、正直なところ、特に読みやすさなどのひどい理由で、早期のエラー処理は適切な方法ではありません。同期コードについても同様です。つまり、すべての関数をtry/catchしないでください。そうしないと、読みやすくなります。

常にエラーを渡し、肯定的なコードフローが再開する時点でエラーを「処理」します。

物事がどこまで進んだかを知る必要がある場合、私が使用するトリックは単純な進行カウンターです:

let progress = "";
A()
.then(a => (progress = "A passed", passA(a)))
.then(B)
.then(b => (progress = "B passed", passB(b)))
.then(C)
.then(c => (progress = "C passed", passC(c)))
.catch(err => (console.log(progress), failAll(err)))
2
jib