web-dev-qa-db-ja.com

Promiseのcatch / thenブロックから戻る方法

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ステートメントでこのアプローチをサポートしていませんか?

42
lixiang

これは、言語の機能では実現できません。ただし、パターンベースのソリューションは利用可能です。

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()行を同じパターンにすることができ、式全体が見やすくなります。

...しかし:

  • errorHandlerが結果を返す場合、チェーンは次の行の成功ハンドラーに進みます。
  • errorHandlerがスローすると、チェーンは次の行のエラーハンドラに進みます。

以前にスローされたエラーと新たにスローされたエラーを区別できるようにエラーハンドラーが記述されていない限り、チェーンからの望ましいジャンプは発生しません。例えば ​​:

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.
    }
}

今:

  • errorHandlerが結果を返す場合、チェーンは次のFunctionNに進みます。
  • errorHandlerがMyCustomErrorをスローすると、チェーン内で繰り返し再スローされ、if(error instanceof MyCustomError)プロトコルに準拠しない最初のエラーハンドラー(たとえば、最終的な.catch())によってキャッチされます。
  • errorHandlerが他のタイプのエラーをスローした場合、チェーンは次のキャッチに進みます。

このパターンは、スローされたエラーのタイプに応じて、チェーンの終わりまでスキップするかどうかの柔軟性が必要な場合に役立ちます。まれな状況。

DEMO

絶縁キャッチ

別の解決策は、各.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()はメインチェーンから隔離されません。

さて、

  • errorHandlerが結果を返す場合、チェーンは次の行に進みます。
  • errorHandlerが好きなものをスローすると、チェーンは直接finalErrorHandlerに進みます。

スローされるエラーのタイプに関係なく、常にチェーンの最後までスキップする場合は、このパターンを使用します。カスタムエラーコンストラクターは不要であり、エラーハンドラーを特別な方法で記述する必要はありません。

DEMO

使用例

どのパターンを選択するかは、すでに与えられている考慮事項によって決まりますが、プロジェクトチームの性質によっても決まります。

  • 一人のチーム-あなたはすべてを書いて問題を理解します-あなたが自由に選択できるなら、あなたの個人的な好みで走ります。
  • 複数人チーム-1人がマスターチェーンを作成し、他のさまざまな人が関数とそのエラーハンドラを作成します-可能であれば、Insulated Catchesを選択します-すべてがマスターチェーンの制御下にあるため、その特定の方法でエラーハンドラを記述します。
62
Roamer-1888

まず、このセクションのコードでよくある間違いを見つけます。これがサンプルコードブ​​ロックです。

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つの簡単なルール。

  1. 誰も拒否をキャッチしなかった場合、約束の連鎖を直ちに停止し、元の拒否が約束の最終状態になります。後続のハンドラーは呼び出されません。
  2. 約束拒否がキャッチされ、何も返されないか、拒否ハンドラから通常の値が返される場合、拒否は処理されたと見なされ、約束チェーンが続行され、後続のハンドラが呼び出されます。リジェクトハンドラーから返されたものはすべてプロミスの現在の値になり、リジェクトが発生しなかったかのようになります(このレベルの解決ハンドラーが呼び出されなかったことを除き、代わりにリジェクトハンドラーが呼び出されました)。
  3. 約束拒否がキャッチされ、拒否ハンドラからエラーをスローするか、拒否された約束を返すと、チェーン内の次の拒否ハンドラまですべての解決ハンドラがスキップされます。リジェクトハンドラが存在しない場合、Promiseチェーンは停止し、新しく作成されたエラーがPromiseの最終状態になります。

this jsFiddle でいくつかの例を見ることができます。

  1. 拒否ハンドラから通常の値を返すと、次の.then()解決ハンドラが呼び出されます(たとえば、通常の処理が続行されます)。

  2. 拒否ハンドラーをスローすると、通常の解決処理が停止し、拒否ハンドラーまたはチェーンの最後に到達するまで、すべての解決ハンドラーがスキップされます。これは、解決ハンドラーで予期しないエラーが見つかった場合にチェーンを停止する効果的な方法です(これはあなたの質問だと思います)。

  3. リジェクトハンドラーが存在しないと、通常のリゾルブ処理が停止し、リジェクトハンドラーまたはチェーンの最後に到達するまで、すべてのリゾルバーハンドラーがスキップされます。

12
jfriend00

リクエスト中に残りのチェーン全体をスキップする組み込み機能はありません。ただし、各キャッチを通じて特定のエラーをスローすることにより、この動作を模倣できます。

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ブロックに進む前にコンソールに記録されます。

4
rrowland