プロミスチェーンで複数のキャッチを処理する


125

私はまだプロミスにかなり慣れておらず、現在はbluebirdを使用していますが、どのように対処すればよいかよくわからないというシナリオがあります。

たとえば、次のようなExpressアプリ内にプロミスチェーンがあります。

repository.Query(getAccountByIdQuery)
        .catch(function(error){
            res.status(404).send({ error: "No account found with this Id" });
        })
        .then(convertDocumentToModel)
        .then(verifyOldPassword)
        .catch(function(error) {
            res.status(406).send({ OldPassword: error });
        })
        .then(changePassword)
        .then(function(){
            res.status(200).send();
        })
        .catch(function(error){
            console.log(error);
            res.status(500).send({ error: "Unable to change password" });
        });

だから私が求めている行動は:

  • IDでアカウントを取得します
  • この時点で拒否された場合は、爆撃してエラーを返します
  • エラーがなければ、ドキュメントをモデルに変換します
  • データベースドキュメントでパスワードを確認する
  • パスワードが一致しない場合、爆破して別のエラーを返します
  • エラーがなければ、パスワードを変更します
  • その後、成功を返します
  • 他に問題が発生した場合は、500を返します

したがって、現在のところ、キャッチはチェーンを停止しないようで、それは理にかなっているので、エラーに基づいてチェーンを特定のポイントで強制的に停止する方法があるのか​​、またはより良い方法があるのか​​と思います。の場合があるように、これを構造化して何らかの形の分岐動作を取得しif X do Y else Zます。

どんな助けでも素晴らしいでしょう。


あなたは再投球するか、早期復帰することができますか?
Pieter21、2014

回答:


126

この動作は、同期スローとまったく同じです。

try{
    throw new Error();
} catch(e){
    // handle
} 
// this code will run, since you recovered from the error!

これが、.catchエラーから回復できるようにするためのポイントの半分です。状態がまだエラーであることを通知するために再スローすることが望ましい場合があります。

try{
    throw new Error();
} catch(e){
    // handle
    throw e; // or a wrapper over e so we know it wasn't handled
} 
// this code will not run

ただし、エラーは後のハンドラーでキャッチされるため、これだけでは機能しません。ここでの本当の問題は、一般化された "HANDLE ANYTHING"エラーハンドラーは一般に悪い習慣であり、他のプログラミング言語やエコシステムでは非常に嫌われているということです。このため、Bluebirdは型付きおよび述語のキャッチを提供します。

追加の利点は、ビジネスロジックが要求/応答サイクルをまったく認識している必要がない(および認識してはならない)ことです。クライアントが取得するHTTPステータスとエラーを決定するのはクエリの責任ではありません。後でアプリが大きくなると、クライアントに送信するものからビジネスロジック(DBをクエリする方法とデータを処理する方法)を分離する必要がある場合があります。 (httpステータスコード、テキスト、応答)。

これが私があなたのコードを書く方法です。

まず、を.QueryスローしNoSuchAccountErrorPromise.OperationalErrorBluebirdがすでに提供しているをサブクラス化します。エラーをサブクラス化する方法がわからない場合は、お知らせください。

さらにそれをサブクラス化してAuthenticationError、次のようなことをします:

function changePassword(queryDataEtc){ 
    return repository.Query(getAccountByIdQuery)
                     .then(convertDocumentToModel)
                     .then(verifyOldPassword)
                     .then(changePassword);
}

ご覧のとおり、非常にクリーンであり、プロセスで何が発生するかについての取扱説明書のようにテキストを読むことができます。また、リクエスト/レスポンスからも分離されています。

ここで、ルートハンドラーから次のように呼び出します。

 changePassword(params)
 .catch(NoSuchAccountError, function(e){
     res.status(404).send({ error: "No account found with this Id" });
 }).catch(AuthenticationError, function(e){
     res.status(406).send({ OldPassword: error });
 }).error(function(e){ // catches any remaining operational errors
     res.status(500).send({ error: "Unable to change password" });
 }).catch(function(e){
     res.status(500).send({ error: "Unknown internal server error" });
 });

このように、ロジックはすべて1か所にあり、クライアントへのエラーを処理する方法の決定はすべて1か所にあり、それらが互いに散らかることはありません。


11
.catch(someSpecificError)特定のエラーの中間ハンドラーが存在する理由は、特定のタイプのエラー(無害)をキャッチし、それを処理して次のフローを続行する場合であることを追加できます。たとえば、一連の処理を行うスタートアップコードがあります。最初にディスクから構成ファイルを読み取ることですが、その構成ファイルが見つからない場合はOKエラー(プログラムはデフォルトで組み込まれています)なので、その特定のエラーを処理して残りのフローを続行できます。後日まで離れない方がよいクリーンアップもあるかもしれません。
jfriend00 2014

1
「これが.catchの要点の半分です。エラーから回復できるようになる」と、そのことが明確になったと思いますが、それを明らかにしてくれてありがとう、それは良い例です。
Benjamin Gruenbaum 2014

1
ブルーバードが使用されていない場合はどうなりますか?プレーンなes6 promiseには、catchに渡される文字列エラーメッセージのみがあります。
時計屋

3
@clocksmithとES6を使用すると、すべてをキャッチしてinstanceof手動でチェックを行うことができます。
Benjamin Gruenbaum 2017年

1
Errorオブジェクトをサブクラス化するためのリファレンスを探している方は、bluebirdjs.com / docs / api / catch.html#filtered-catchをお読みください 。記事はまた、ここで与えられた複数キャッチの回答をかなり再現しています。
mummybot 2017年

47

.catchtry-catchこれはステートメントのように機能します。つまり、最後に必要なキャッチは1つだけです。

repository.Query(getAccountByIdQuery)
        .then(convertDocumentToModel)
        .then(verifyOldPassword)
        .then(changePassword)
        .then(function(){
            res.status(200).send();
        })
        .catch(function(error) {
            if (/*see if error is not found error*/) {
                res.status(404).send({ error: "No account found with this Id" });
            } else if (/*see if error is verification error*/) {
                res.status(406).send({ OldPassword: error });
            } else {
                console.log(error);
                res.status(500).send({ error: "Unable to change password" });
            }
        });

1
ええ、私はこれを知っていましたが、巨大なエラーチェーンを実行したくなかったので、必要なときに、それを実行する方が読みやすかったようです。したがって、最後にすべてをキャッチしますが、意図に関してより記述的であるため、型付きエラーの考え方が好きです。
Grofit 2014

8
@Grofitは価値がある-Bluebirdでの型付きキャッチ Petka(Esailija)の最初のアイデアでした:)彼を説得する必要はありません。JSの多くの人は概念をあまり知らないので、彼はあなたを混乱させたくなかったと思います。
Benjamin Gruenbaum 2014

17

エラーに基づいて何らかの方法でチェーンを強制的に停止させる方法があるかどうか疑問に思っています

いいえ。チェーンが最後までバブリングする例外をスローしない限り、チェーンを「終了」することはできません。その方法については、Benjamin Gruenbaumの回答を参照してください。

彼のパターンの派生は、エラータイプを区別することではなく、単一の汎用ハンドラーから送信できるフィールドstatusCodebodyフィールドを持つエラーを使用すること.catchです。アプリケーションの構造によっては、彼の解決策はよりクリーンな場合もあります。

または、これを構造化して何らかの形の分岐動作を得るより良い方法がある場合

はい、promiseで分岐できます。ただし、これはチェーンを残してネストに「戻る」ことを意味します-ネストされたif-elseまたはtry-catchステートメントで行うのと同じです。

repository.Query(getAccountByIdQuery)
.then(function(account) {
    return convertDocumentToModel(account)
    .then(verifyOldPassword)
    .then(function(verification) {
        return changePassword(verification)
        .then(function() {
            res.status(200).send();
        })
    }, function(verificationError) {
        res.status(406).send({ OldPassword: error });
    })
}, function(accountError){
    res.status(404).send({ error: "No account found with this Id" });
})
.catch(function(error){
    console.log(error);
    res.status(500).send({ error: "Unable to change password" });
});

5

私はこのようにしてきました:

あなたは最終的にあなたの捕獲物を残します。そして、チェーンの途中でエラーが発生した場合にスローします。

    repository.Query(getAccountByIdQuery)
    .then((resultOfQuery) => convertDocumentToModel(resultOfQuery)) //inside convertDocumentToModel() you check for empty and then throw new Error('no_account')
    .then((model) => verifyOldPassword(model)) //inside convertDocumentToModel() you check for empty and then throw new Error('no_account')        
    .then(changePassword)
    .then(function(){
        res.status(200).send();
    })
    .catch((error) => {
    if (error.name === 'no_account'){
        res.status(404).send({ error: "No account found with this Id" });

    } else  if (error.name === 'wrong_old_password'){
        res.status(406).send({ OldPassword: error });

    } else {
         res.status(500).send({ error: "Unable to change password" });

    }
});

他の関数はおそらく次のようになります。

function convertDocumentToModel(resultOfQuery) {
    if (!resultOfQuery){
        throw new Error('no_account');
    } else {
    return new Promise(function(resolve) {
        //do stuff then resolve
        resolve(model);
    }                       
}

4

おそらくパーティーには少し遅れますが、.catchここに示すようにネストすることは可能です:

Mozilla Developer Network-プロミスの使用

編集:要求された機能を一般的に提供するため、これを送信しました。ただし、この特定のケースではありません。すでに他の人が詳細に説明しているように.catch、エラーを回復することになっています。たとえば、明示的ながない場合はがそれを解決するため、複数の .catchコールバックでクライアントに応答を送信することはできません。チェーンが実際には解決されていなくてもトリガーが進行し、フォローがトリガーされて送信される可能性がありますクライアントへの別の応答。エラーを引き起こし、おそらくあなたのやり方を投げます。この複雑な文章があなたにとって何らかの意味をなすことを願っています。.catchreturn undefined.then.catchUnhandledPromiseRejection


1
@AntonMenshovあなたは正しいです。私は彼の希望の動作が営巣してまだ可能ではない理由を説明する、私の答えを拡大
denkquer

2

代わりに.then().catch()...できます.then(resolveFunc, rejectFunc)。このプロミスチェーンは、途中で物事を処理する場合に適しています。これが私がそれを書き直す方法です:

repository.Query(getAccountByIdQuery)
    .then(
        convertDocumentToModel,
        () => {
            res.status(404).send({ error: "No account found with this Id" });
            return Promise.reject(null)
        }
    )
    .then(
        verifyOldPassword,
        () => Promise.reject(null)
    )
    .then(
        changePassword,
        (error) => {
            if (error != null) {
                res.status(406).send({ OldPassword: error });
            }
            return Promise.Promise.reject(null);
        }
    )
    .then(
        _ => res.status(200).send(),
        error => {
            if (error != null) {
                console.error(error);
                res.status(500).send({ error: "Unable to change password" });
            }
        }
    );

注:if (error != null)最新のエラーと対話するためのハックのビットです。


1

上記のBenjamin Gruenbaumの答えは複雑なロジックシーケンスの最良の解決策だと思いますが、これはより単純な状況に対する私の代替策です。errorEncounteredフラグを一緒に使用して、return Promise.reject()後続のthenor catchステートメントをスキップします。したがって、次のようになります。

let errorEncountered = false;
someCall({
  /* do stuff */
})
.catch({
  /* handle error from someCall*/
  errorEncountered = true;
  return Promise.reject();
})
.then({
  /* do other stuff */
  /* this is skipped if the preceding catch was triggered, due to Promise.reject */
})
.catch({
  if (errorEncountered) {
    return;
  }
  /* handle error from preceding then, if it was executed */
  /* if the preceding catch was executed, this is skipped due to the errorEncountered flag */
});

then / catchペアが3つ以上ある場合は、おそらくベンジャミングルーエンバウムのソリューションを使用する必要があります。しかし、これは簡単な設定で機能します。

ファイナルはcatchだけではreturn;なく、return Promise.reject();それ以降thenはスキップする必要がないことに注意してください。これは、Nodeが好まない未処理のPromise拒否としてカウントされます。上記のように、決勝戦catchは平和的に解決された約束を返します。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.