web-dev-qa-db-ja.com

AngularJS-キャッチされた例外を再スローすることを約束します

次のコードでは、$ q promiseのcatch関数によって例外がキャッチされます。

// Fiddle - http://jsfiddle.net/EFpn8/6/
f1().then(function(data) {
        console.log("success 1: "+data)
        return f2();
    })
    .then(function(data) {console.log("success 2: "+data)})
    .catch(function(data) {console.log("error: "+data)});

function f1() {
    var deferred = $q.defer();
    // An exception thrown here is not caught in catch
    // throw "err";
    deferred.resolve("done f1");        
    return deferred.promise;
}

function f2() {
    var deferred = $q.defer();
    // An exception thrown here is handled properly
    throw "err";
    deferred.resolve("done f2");        
    return deferred.promise;
}  

ただし、コンソールログ出力を見ると、次のことがわかります。

enter image description here

例外はAngularでキャッチされましたが、ブラウザのエラー処理によってもキャッチされました。この動作はQライブラリで再現されます。

バグですか? $ qで本当に例外をキャッチするにはどうすればよいですか?

17
VitalyB

AngularJSバージョン1.6で修正

この動作の理由は、たとえばプログラミングエラーが原因である可能性があるため、キャッチされなかったエラーは通常の拒否とは異なるためです。実際には、ネイティブのPromiseも他の一般的なPromiseライブラリも、スローされたエラーと通常の拒否を区別しないため、これはユーザーにとって混乱または望ましくないことが判明しました。 (注:この動作はPromises/A +仕様に反するものではありませんが、規定されていません。)

$ q:

e13eea のため、promiseのonFulfilledまたはonRejectionハンドラーからスローされたエラーは、通常の拒否とまったく同じように扱われます。以前は、それは$exceptionHandler()にも渡されていました(理由としてエラーを使用してpromiseを拒否することに加えて)。

新しい動作は、_$q_に依存するすべてのサービス/コントローラー/フィルターなどに適用されます(_$http_や_$route_などの組み込みサービスを含む)。たとえば、_$http's transformRequest/Response_関数またはルートのredirectTo関数、およびルートのresolveオブジェクトで指定された関数は、エラーをスローした場合に$exceptionHandler()を呼び出さなくなりました。それ以外は、すべてが同じように動作し続けます。つまり、プロミスは拒否され、ルートの移行はキャンセルされ、_$routeChangeError_イベントがブロードキャストされます。

-AngularJS開発者ガイド-V1.5からV1.6への移行-$ q

5
georgeawg

Angularの$qは、スローされたエラーがログに記録される規則を使用します関係なくキャッチされます。代わりに、拒否を通知する場合は、次のようにreturn $q.reject(...する必要があります。

function f2() {
    var deferred = $q.defer();
    // An exception thrown here is handled properly
    return $q.reject(new Error("err"));//throw "err";
    deferred.resolve("done f2");        
    return deferred.promise;
}  

これは、SyntaxErrorのようなエラーから拒否を区別するためです。個人的には、これは私が同意しない設計上の選択ですが、$qが小さいため、信頼できる未処理の拒否検出メカニズムを実際に組み込むことはできないため、理解できます。 Bluebirdのような強力なライブラリでは、この種のことは必要ありません。

補足として-決して文字列をスローしないでください:そのようにスタックトレースを見逃します。

16

バグですか?

いいえ。 $ qのソース を見ると、コールバックでスローされた例外に応答するために、意図的なtry/catchブロックが作成されていることがわかります。

  1. deferred.rejectに電話したように、約束を拒否する
  2. 登録済みのAngular例外ハンドラーを呼び出す。 $ exceptionHandlerdocs に見られるように、これのデフォルトの動作は、エラーとしてブラウザーコンソールにログを記録することです。これはあなたが観察したことです。

...ブラウザのエラー処理にも巻き込まれました

明確にするために、例外はブラウザによって直接処理されませんが、Angularがconsole.errorを呼び出したため、エラーとして表示されます

$ qで本当に例外をキャッチするにはどうすればよいですか?

コールバックは、現在の呼び出しスタックがクリアされたときに実行されるため、外部関数をtry/catchブロックでラップすることはできません。ただし、2つのオプションがあります。

  • コールバック内で、例外をスローする可能性のあるコードの周囲にtry/catchブロックを配置します。

    f1().then(function(data) {
      try {
        return f2();
      } catch(e) {
        // Might want convert exception to rejected promise
        return $q.reject(e);
      }
    })
    
  • $ exceptionHandler実装をオーバーライドする方法 のように、Angularの$exceptionHandlerサービスの動作を変更します。まったく何もしないように変更できる可能性があるため、コンソールのエラーログには何も記録されませんが、お勧めすることはないと思います。

6
Michal Charemza

Deferredは、promiseを構築するための時代遅れで本当にひどい方法であり、コンストラクターを使用すると、この問題などが解決されます。

// This function is guaranteed to fulfill the promise contract
// of never throwing a synchronous exception, using deferreds manually
// this is virtually impossible to get right
function f1() {
    return new Promise(function(resolve, reject) {
        // code
    });
}

angular promisesが上記をサポートしているかどうかはわかりませんが、そうでない場合は、次のように実行できます。

function createPromise(fn) {
    var d = $q.defer();
    try {
        fn(d.resolve.bind(d), d.reject.bind(d));
    }
    catch (e) {
        d.reject(e);
    }
    return d.promise;
}

使用法はpromiseコンストラクターと同じです。

function f1() {
    return createPromise(function(resolve, reject){
        // code
    });
}
4
Esailija

これは、新しい$ q構築関数、.finally()の使用、拒否、およびpromiseチェーンの伝播を示すサンプルテストです。

iit('test',inject(function($q, $timeout){
    var finallyCalled = false;
    var failValue;

    var promise1 = $q.when(true)
          .then(function(){
            return $q(function(resolve,reject){
              // Reject promise1
              reject("failed");
            });
          })
          .finally(function(){
            // Always called...
            finallyCalled = true;

            // This will be ignored
            return $q.when('passed');
          });

    var promise2 = $q.when(promise1)
          .catch(function(value){
            // Catch reject of promise1
            failValue = value;

            // Continue propagation as resolved
            return value+1;

            // Or continue propagation as rejected
            //return $q.reject(value+2);
          });

    var updateFailValue = function(val){ failValue = val; };

    $q.when(promise2)
      .then( updateFailValue )
      .catch(updateFailValue );

    $timeout.flush();

    expect( finallyCalled ).toBe(true);
    expect( failValue ).toBe('failed1');

}));
0
user2747330