なぜPromise.catchハンドラー内にスローできないのですか?


127

なぜErrorcatchコールバックの内部をスローして、他のスコープにあるかのようにプロセスにエラーを処理させないのはなぜですか?

console.log(err)何もしなければ、何も出力されず、何が起こったのかわかりません。プロセスは終了しました...

例:

function do1() {
    return new Promise(function(resolve, reject) {
        throw new Error('do1');
        setTimeout(resolve, 1000)
    });
}

function do2() {
    return new Promise(function(resolve, reject) {
        setTimeout(function() {
            reject(new Error('do2'));
        }, 1000)
    });
}

do1().then(do2).catch(function(err) {
    //console.log(err.stack); // This is the only way to see the stack
    throw err; // This does nothing
});

コールバックがメインスレッドで実行される場合、なぜErrorブラックホールに飲み込まれるのですか?


11
ブラックホールに飲み込まれない。それは.catch(…)戻る約束を拒否します。
Bergi


の代わりに.catch((e) => { throw new Error() }).catch((e) => { return Promise.reject(new Error()) }).catch((e) => Promise.reject(new Error()))
書面

1
@chharveyコメント内のすべてのコードスニペットの動作はまったく同じですが、最初のものは明らかに最も明確です。
СергейГринько

回答:


157

他の人が説明しているように、「ブラックホール」は、aの内部にスローする.catchと、プロミスが拒否されてチェーンが継続し、キャッチがなくなるため、チェーンが終了せず、エラーが発生します(悪い!)

もう1つキャッチを追加して、何が起こっているかを確認します。

do1().then(do2).catch(function(err) {
    //console.log(err.stack); // This is the only way to see the stack
    throw err; // Where does this go?
}).catch(function(err) {
    console.log(err.stack); // It goes here!
});

チェーンの途中でのキャッチは、ステップが失敗してもチェーンを続行したい場合に役立ちますが、再スローは、情報のロギングやクリーンアップのステップなどを行っ後に失敗続ける場合に役立ちます。スローされます。

騙す

最初に意図したとおりに、エラーをWebコンソールのエラーとして表示するには、次のトリックを使用します。

.catch(function(err) { setTimeout(function() { throw err; }); });

行番号も存続するので、Webコンソールのリンクを使用すると、(元の)エラーが発生したファイルと行に直接移動できます。

なぜ機能するのか

promiseのフルフィルメントまたは拒否ハンドラとして呼び出される関数の例外は、自動的に返されるはずのpromiseの拒否に変換されます。関数を呼び出すpromiseコードがこれを処理します。

一方、setTimeoutによって呼び出される関数は、常にJavaScriptの安定した状態から実行されます。つまり、JavaScriptイベントループの新しいサイクルで実行されます。例外は何もキャッチされず、Webコンソールに到達します。にerrは元のスタック、ファイル、行番号など、エラーに関するすべての情報が保持されているため、引き続き正しく報告されます。


3
ジブ、それは面白いトリックです。なぜそれが機能するのか理解していただけませんか。
ブライアンキース

8
そのトリックについて:ログに記録したいので投げているので、直接ログに記録しないのはなぜですか?このトリックは、「ランダム」なときにキャッチできないエラーをスローします。しかし、例外(およびPromiseがそれらを処理する方法)の全体的な考え方は、エラーをキャッチして処理するのは呼び出し側の責任にすることです。このコードは、呼び出し元がエラーを処理することを事実上不可能にします。それを処理する関数を作ってみませんか?function logErrors(e){console.error(e)}それからのように使ってくださいdo1().then(do2).catch(logErrors)。回答自体はすばらしいです。+ 1
Stijn de Witt 2016

3
@jib私は、この場合のように多かれ少なかれ接続されている多くの約束を含むAWSラムダを書いています。エラーが発生した場合にAWSのアラームと通知を利用するには、ラムダクラッシュでエラーをスローする必要があります(私はそう思います)。トリックはこれを取得する唯一の方法ですか?
masciugo 2016

2
@StijndeWitt私の場合、window.onerrorイベントハンドラーでエラーの詳細をサーバーに送信しようとしました。setTimeoutトリックを行うことによってのみ、これを行うことができます。それ以外の場合window.onerrorは、Promiseで発生したエラーに関する情報を聞くことはありません。
hudidit 2017

1
@hudiditはまだ、それはだwheter console.logpostErrorToServer、あなただけの何をすべきか行うことができます。どんなコードでwindow.onerrorも、別の関数に分解して2か所から呼び出すことができない理由はありません。それはおそらくsetTimeoutラインよりもさらに短いでしょう。
Stijn de Witt 2017

46

ここで理解しておくべき重要なこと

  1. 関数thencatch関数の両方が新しいpromiseオブジェクトを返します。

  2. スローするか、明示的に拒否すると、現在のプロミスが拒否状態に移行します。

  3. 以来thencatch新しい約束のオブジェクトを返し、それらを連鎖させることができます。

  4. promiseハンドラー(thenまたはcatch)内でスローまたは拒否すると、チェーンパスの下の次の拒否ハンドラーで処理されます。

  5. jfriend00で述べたように、ハンドラーthencatchハンドラーは同期的に実行されません。ハンドラーがスローすると、すぐに終了します。したがって、スタックはほどかれ、例外は失われます。そのため、例外をスローすると現在の約束が拒否されます。


あなたの場合、あなたはオブジェクトをdo1投げることによって内部を拒否していErrorます。これで、現在のpromiseは拒否された状態になり、コントロールは次のハンドラー(then今回のケース)に移されます。

以来thenハンドラが拒否ハンドラを持っていない、do2まったく実行されません。console.log中身で確認できます。現在のpromiseには拒否ハンドラがないため、前のpromiseの拒否値で拒否され、コントロールは次のハンドラであるに転送されcatchます。

catch拒否ハンドラと同様に、console.log(err.stack);内部で行うと、エラースタックトレースを確認できます。今、あなたはErrorそれからオブジェクトを投げているので、によって返されたpromise catchも拒否された状態になります。

に拒否ハンドラをアタッチしていないcatchため、拒否を確認することはできません。


このように、チェーンを分割してこれをよりよく理解できます

var promise = do1().then(do2);

var promise1 = promise.catch(function (err) {
    console.log("Promise", promise);
    throw err;
});

promise1.catch(function (err) {
    console.log("Promise1", promise1);
});

あなたが得る出力は次のようなものになります

Promise Promise { <rejected> [Error: do1] }
Promise1 Promise { <rejected> [Error: do1] }

catchハンドラー1の内部では、promiseobject の値が拒否されています。

catch同様に、ハンドラー1 によって返されたプロミスも拒否されたのと同じエラーでpromise拒否され、2番目のcatchハンドラーでそれを監視しています。


3
.then()ハンドラーが非同期であること(スタックは実行される前に巻き戻される)を追加する価値もあるので、ハンドラー内の例外は拒否に変換する必要があります。そうでない場合、ハンドラーをキャッチする例外ハンドラーはありません。
jfriend00

7

setTimeout()上記の方法を試しました...

.catch(function(err) { setTimeout(function() { throw err; }); });

厄介なことに、これは完全にテスト不可能であることがわかりました。非同期エラーをスローしているtry/catchため、catchエラーがスローされるまでにがリッスンを停止するため、ステートメント内でラップすることはできません。

完璧に機能するリスナーを使用することに戻りましたが、JavaScriptの使用方法が意図されているため、非常にテスト可能でした。

return new Promise((resolve, reject) => {
    reject("err");
}).catch(err => {
    this.emit("uncaughtException", err);

    /* Throw so the promise is still rejected for testing */
    throw err;
});

3
Jestには、この状況を処理するタイマーモックがあります。
jordanbtucker 2017

2

仕様によると(3.III.dを参照)

d。呼び出すと、例外e、
  aがスローされます。resolvePromiseまたはrejectPromiseが呼び出されている場合は、無視してください。
  b。それ以外の場合は、理由としてeを使用して約束を拒否します。

つまり、then関数で例外をスローすると、例外がキャッチされ、約束が拒否されます。catchここでは意味をなさないでください。これは、.then(null, function() {})

未処理の拒否をコードに記録したいと思います。ほとんどのpromiseライブラリunhandledRejectionはそれを実行します。ここでそれについての議論に関連する要点です。


このunhandledRejectionフックはサーバー側のJavaScript用であり、クライアント側ではブラウザーごとに異なるソリューションがあることは言及に値します。まだ標準化していませんが、ゆっくりと確実に実現できています。
Benjamin Gruenbaum 2015年

1

私はこれが少し遅れていることを知っていますが、このスレッドに出くわし、どのソリューションも簡単に実装することができなかったので、自分で考えました。

次のように、promiseを返す小さなヘルパー関数を追加しました。

function throw_promise_error (error) {
 return new Promise(function (resolve, reject){
  reject(error)
 })
}

次に、エラーをスローする(そしてプロミスを拒否する)プロミスチェーンの特定の場所がある場合は、次のように、構築されたエラーで上記の関数から単純に戻ります。

}).then(function (input) {
 if (input === null) {
  let err = {code: 400, reason: 'input provided is null'}
  return throw_promise_error(err)
 } else {
  return noterrorpromise...
 }
}).then(...).catch(function (error) {
 res.status(error.code).send(error.reason);
})

このようにして、プロミスチェーンの内部から余分なエラーをスローすることができます。「通常の」プロミスエラーも処理する場合は、キャッチを拡張して「自己スロー」エラーを個別に処理します。

これが役に立てば幸いです、それが私の最初のスタックオーバーフローの答えです!


Promise.reject(error)代わりにnew Promise(function (resolve, reject){ reject(error) })(とにかくreturnステートメントが必要です)
Funkodebat

0

はい、飲み込みエラーを約束し.catchます。他の回答で詳しく説明されているように、エラーはでしかキャッチできません。Node.jsを使用していて、通常のthrow動作を再現したい場合は、スタックトレースをコンソールに出力してプロセスを終了します。

...
  throw new Error('My error message');
})
.catch(function (err) {
  console.error(err.stack);
  process.exit(0);
});

1
いいえ、それはあなたが持っているすべてのプロミスチェーンの最後にそれを置く必要があるので十分ではありません。イベントではunhandledRejection
ベルギ2015年

はい、それはあなたがあなたの約束を連鎖させることを前提としているので、出口は最後の関数であり、それは後にキャッチされません。あなたが言及するイベントは、Bluebirdを使用している場合にのみだと思います。
ヘスス・カレラ

Bluebird、Q、いつ、ネイティブの約束…それはおそらく標準になるでしょう。
Bergi、2015年
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.