JavaScript Promiseでプログラミングする際の「then」および「catch」の使用方法に関するチュートリアルが多数あります。ただし、これらのチュートリアルはすべて、重要なポイントを見逃しているようです。then/ catchブロックから戻ってPromiseチェーンを解除します。この問題を説明するために、いくつかの同期コードから始めましょう。
try {
someFunction();
} catch (err) {
if (!(err instanceof MyCustomError))
return -1;
}
someOtherFunction();
本質的に、私はキャッチされたエラーをテストしていますが、それがエラーではない場合、呼び出し元に戻ります。そうでない場合、プログラムは続行します。ただし、このロジックはPromiseでは機能しません。
Promise.resolve(someFunction).then(function() {
console.log('someFunction should throw error');
return -2;
}).catch(function(err) {
if (err instanceof MyCustomError) {
return -1;
}
}).then(someOtherFunction);
このロジックは、特定の方法で関数が失敗するような単体テストの一部に使用されます。 catchをthenブロックに変更しても、then/catchブロックから返されるものはすべてチェーンに沿って伝播するPromiseになるため、一連のチェーンされたPromiseを壊すことはできません。
Promiseがこのロジックを達成できるかどうかは疑問です。そうでない場合、なぜですか? Promiseチェーンが決して壊れないことは私にとって非常に奇妙です。ありがとう!
編集2015年8月16日:これまでの回答によると、thenブロックによって返された拒否されたPromiseは、Promiseチェーンを介して伝播し、キャッチされる(処理される)まで後続のthenブロックをすべてスキップします。この動作は、次の同期コードを模倣するだけなので、よく理解されています(アプローチ1)。
try {
Function1();
Function2();
Function3();
Function4();
} catch (err) {
// Assuming this err is thrown in Function1; Function2, Function3 and Function4 will not be executed
console.log(err);
}
ただし、私が求めていたのは、同期コードの次のシナリオです(アプローチ2)。
try {
Function1();
} catch(err) {
console.log(err); // Function1's error
return -1; // return immediately
}
try {
Function2();
} catch(err) {
console.log(err);
}
try {
Function3();
} catch(err) {
console.log(err);
}
try {
Function4();
} catch(err) {
console.log(err);
}
異なる関数で発生したエラーを別々に処理したいと思います。アプローチ1に示すように、1つのcatchブロックですべてのエラーをキャッチすることは可能です。しかし、その方法では、異なるエラーを区別するために、catchブロック内に大きなswitchステートメントを作成する必要があります。さらに、異なる関数によってスローされたエラーに共通の切り替え可能な属性がない場合、switchステートメントをまったく使用できません。このような状況では、関数呼び出しごとに個別のtry/catchブロックを使用する必要があります。アプローチ2が唯一のオプションである場合があります。 Promiseはthen/catchステートメントでこのアプローチをサポートしていませんか?
これは、言語の機能では実現できません。ただし、パターンベースのソリューションは利用可能です。
2つのソリューションがあります。
前のエラーを再スロー
このパターンは基本的に健全です...
Promise.resolve()
.then(Function1).catch(errorHandler1)
.then(Function2).catch(errorHandler2)
.then(Function3).catch(errorHandler3)
.then(Function4).catch(errorHandler4)
.catch(finalErrorHandler);
Promise.resolve()
は必ずしも必要ではありませんが、すべての.then().catch()
行を同じパターンにすることができ、式全体が見やすくなります。
...しかし:
以前にスローされたエラーと新たにスローされたエラーを区別できるようにエラーハンドラーが記述されていない限り、チェーンからの望ましいジャンプは発生しません。例えば :
function errorHandler1(error) {
if (error instanceof MyCustomError) { // <<<<<<< test for previously thrown error
throw error;
} else {
// do errorHandler1 stuff then
// return a result or
// throw new MyCustomError() or
// throw new Error(), new RangeError() etc. or some other type of custom error.
}
}
今:
if(error instanceof MyCustomError)
プロトコルに準拠しない最初のエラーハンドラー(たとえば、最終的な.catch())によってキャッチされます。このパターンは、スローされたエラーのタイプに応じて、チェーンの終わりまでスキップするかどうかの柔軟性が必要な場合に役立ちます。まれな状況。
絶縁キャッチ
別の解決策は、各.catch(errorHandlerN)
を「絶縁」したままにして、前のエラーではなくits対応するFunctionN
から生じるエラーのみをキャッチするメカニズムを導入することです。
これは、サブチェーンを含む匿名関数を含む成功ハンドラのみをメインチェーンに含めることで実現できます。
Promise.resolve()
.then(function() { return Function1().catch(errorHandler1); })
.then(function() { return Function2().catch(errorHandler2); })
.then(function() { return Function3().catch(errorHandler3); })
.then(function() { return Function4().catch(errorHandler4); })
.catch(finalErrorHandler);
ここでPromise.resolve()
は重要な役割を果たします。これがないと、Function1().catch(errorHandler1)
はメインチェーンに含まれ、catch()
はメインチェーンから隔離されません。
さて、
スローされるエラーのタイプに関係なく、常にチェーンの最後までスキップする場合は、このパターンを使用します。カスタムエラーコンストラクターは不要であり、エラーハンドラーを特別な方法で記述する必要はありません。
使用例
どのパターンを選択するかは、すでに与えられている考慮事項によって決まりますが、プロジェクトチームの性質によっても決まります。
まず、このセクションのコードでよくある間違いを見つけます。これがサンプルコードブロックです。
Promise.resolve(someFunction()).then(function() {
console.log('someFunction should throw error');
return -2;
}).catch(function(err) {
if (err instanceof MyCustomError) {
return -1;
}
}).then(someOtherFunction());
実際に関数を呼び出してその戻り結果を渡すのではなく、.then()
ハンドラーに関数参照を渡す必要があります。したがって、上記のコードはおそらく次のようになります。
Promise.resolve(someFunction()).then(function() {
console.log('someFunction should throw error');
return -2;
}).catch(function(err) {
if (err instanceof MyCustomError) {
// returning a normal value here will take care of the rejection
// and continue subsequent processing
return -1;
}
}).then(someOtherFunction); // just pass function reference here
.then()
ハンドラー内の関数の後に()
を削除したため、関数をすぐに呼び出すのではなく、関数参照を渡すだけです。これにより、Promiseインフラストラクチャが将来プロミスを呼び出すかどうかを決定できます。この間違いを犯していた場合、それは約束がどのように機能するかを完全に失います。
拒否のキャッチに関する3つの簡単なルール。
this jsFiddle でいくつかの例を見ることができます。
拒否ハンドラから通常の値を返すと、次の.then()
解決ハンドラが呼び出されます(たとえば、通常の処理が続行されます)。
拒否ハンドラーをスローすると、通常の解決処理が停止し、拒否ハンドラーまたはチェーンの最後に到達するまで、すべての解決ハンドラーがスキップされます。これは、解決ハンドラーで予期しないエラーが見つかった場合にチェーンを停止する効果的な方法です(これはあなたの質問だと思います)。
リジェクトハンドラーが存在しないと、通常のリゾルブ処理が停止し、リジェクトハンドラーまたはチェーンの最後に到達するまで、すべてのリゾルバーハンドラーがスキップされます。
リクエスト中に残りのチェーン全体をスキップする組み込み機能はありません。ただし、各キャッチを通じて特定のエラーをスローすることにより、この動作を模倣できます。
doSomething()
.then(func1).catch(handleError)
.then(func2).catch(handleError)
.then(func3).catch(handleError);
function handleError(reason) {
if (reason instanceof criticalError) {
throw reason;
}
console.info(reason);
}
キャッチブロックのいずれかがcriticalError
をキャッチした場合、最後までまっすぐスキップしてエラーをスローします。その他のエラーは、次の.then
ブロックに進む前にコンソールに記録されます。