web-dev-qa-db-ja.com

拒否されない約束の連鎖

拒否がプロミスチェーンを介して渡されない理由を理解するのに問題があります。その理由を理解してくれる人がくれることを期待しています。私にとって、一連のプロミスに機能を付加することは、実現する本来のプロミスに依存しているという意図を意味します。説明するのは難しいので、最初に問題のコード例を示します。 (注:この例ではNodeと据え置きノードモジュールを使用しています。これをDojo 1.8.3でテストしたところ、同じ結果が得られました)

var d = require("deferred");

var d1 = d();

var promise1 = d1.promise.then(
    function(wins) { console.log('promise1 resolved'); return wins;},
    function(err) { console.log('promise1 rejected'); return err;});
var promise2 = promise1.then(
    function(wins) { console.log('promise2 resolved'); return wins;},
    function(err) { console.log('promise2 rejected'); return err;});
var promise3 = promise2.then(
    function(wins) { console.log('promise3 resolved'); return wins;},
    function(err) { console.log('promise3 rejected'); return err;});
d1.reject(new Error());

この操作を実行した結果は、次の出力です。

promise1 rejected
promise2 resolved
promise3 resolved

さて、私には、この結果は意味がありません。このプロミスチェーンに接続することにより、それぞれが、d1の正常な解決とチェーンに渡される結果に依存するという意図を意味します。 promise1のpromiseがwins値を受信せず、代わりにエラーハンドラーでerr値を取得する場合、チェーン内の次のpromiseが成功関数を呼び出すにはどうすればよいですか?それ自体が値を取得しなかったため、次のプロミスに意味のある値を渡すことができません。

私が考えていることを説明する別の方法は、ジョン、ジンジャー、ボブの3人です。ジョンはウィジェットショップを所有しています。ジンジャーが自分の店にやって来て、さまざまな色のウィジェットのバッグを要求します。彼には在庫がありませんので、彼は彼にそれらを出荷させるために彼のディストリビューターにリクエストを送ります。その間、彼は彼女にウィジェットのバッグを借りていると述べて、ジンジャーにレインチェックを与えます。ボブは、ジンジャーがウィジェットを取得していることを知り、彼女がウィジェットを使い終わったら青いウィジェットを取得するように要求します。彼女は同意し、彼女にそうすることを述べたメモを彼に与える。現在、Johnのディストリビュータはウィジェットをサプライ品で見つけることができず、製造元はウィジェットをもう作成していないため、Johnに通知し、Johnはウィジェットを取得できないことをGingerに通知します。ボブは自分で何も手に入らなかったときに、ジンジャーから青いウィジェットをどのように取得できますか?

私がこの問題について持っている3番目のより現実的な視点はこれです。データベースに更新する2つの値があるとします。 1つは他のIDに依存していますが、データベースに挿入して結果を取得するまで、IDを取得できません。その上、最初の挿入はデータベースからのクエリに依存しています。データベース呼び出しは、2つの呼び出しをシーケンスにチェーンするために使用するpromiseを返します。

var promise = db.query({parent_id: value});
promise.then(function(query_result) {
    var first_value = {
        parent_id: query_result[0].parent_id
    }
    var promise = db.put(first_value);
    promise.then(function(first_value_result) {
        var second_value = {
            reference_to_first_value_id: first_value_result.id
        }
        var promise = db.put(second_value);
        promise.then(function(second_value_result) {
            values_successfully_entered();
        }, function(err) { return err });
    }, function(err) { return err });
}, function(err) { return err });

さて、この状況では、db.queryが失敗した場合、最初のthenのerr関数を呼び出します。しかし、それは次の約束の成功関数を呼び出します。そのpromiseは最初の値の結果を期待していますが、代わりにエラーハンドラー関数からエラーメッセージを取得します。

それで、私の質問は、成功関数のエラーをテストする必要がある場合、なぜエラー処理関数があるのですか?

この長さでごめんなさい。私はそれを別の方法で説明する方法を知りませんでした。

更新と修正

(注:以前にいくつかのコメントに対して行った応答を削除しました。そのため、誰かが私の応答にコメントした場合、そのコメントはコンテキストの外にあるように見える可能性があります。削除しました。申し訳ありませんが、これはできるだけ短くします。)

回答してくださった皆さんありがとうございました。私の質問、特に私の疑似コードをあまりうまく書けなかったことを最初に皆に謝罪したいと思います。私はそれを短くしようとすることにあまりにも積極的でした。

Bergiの返答のおかげで、論理に誤りがあったと思います。私が抱えていた問題を引き起こしている別の問題を見落としていたのではないかと思います。これにより、プロミスチェーンの動作が、思ったよりも異なる可能性があります。私はまだコードのさまざまな要素をテストしているので、私がまだ間違っていることを確認するための適切な質問を作成することもできません。私はあなたにすべてを更新したいと思いました、そしてあなたの助けに感謝します。

34
Jordan

私には、この結果は意味がありません。このプロミスチェーンに接続することにより、各プロミスは、d1の正常な解決とチェーンに渡される結果に依存するという意図を意味します。

いいえ。あなたが説明しているのはチェーンではなく、すべてのコールバックをd1に接続するだけです。ただし、thenを使用してチェーンを作成する場合、promise2の結果は、promise1の解決と、thenコールバックによる処理に依存します。

ドキュメントの状態:

コールバックの結果の新しいプロミスを返します。

.thenメソッドは通常、 Promises/A仕様 (またはさらに厳密な Promsises/A + one )の観点から見られます。つまり、コールバックシェルはpromise2の解決になるために同化されるpromiseを返し、成功/エラーハンドラーがない場合、それぞれの結果はpromise2に直接渡されるため、単純にハンドラを省略してエラーを伝播します。

ただし、エラーがhandledの場合、結果のpromise2は修正済みと見なされ、その値で満たされます。これを望まない場合は、try-catch句の場合と同様に、re -throw the errorする必要があります。あるいは、ハンドラーから(拒否される)拒否されたプロミスを返すこともできます。 Dojoが拒否する方法が何かはわかりませんが、次のようになります。

var d1 = d();

var promise1 = d1.promise.then(
    function(wins) { console.log('promise1 resolved'); return wins;},
    function(err) { console.log('promise1 rejected'); throw err;});
var promise2 = promise1.then(
    function(wins) { console.log('promise2 resolved'); return wins;},
    function(err) { console.log('promise2 rejected'); throw err;});
var promise3 = promise2.then(
    function(wins) { console.log('promise3 resolved'); return wins;},
    function(err) { console.log('promise3 rejected'); throw err;});
d1.reject(new Error());

ボブは自分で何も手に入らなかったときに、ジンジャーから青いウィジェットをどのように取得できますか?

彼はできないはずです。エラーハンドラーがない場合、ウィジェットは残っていないというメッセージ(((配布元から)Johnから)Ginger)を認識します。それでも、Gingerがそのケースのエラーハンドラーを設定した場合、ジョンまたは彼のディストリビューターに青いものが残っていない場合、ボブに自分の小屋から緑のものを与えることによって、ボブにウィジェットを与えるという約束を果たす可能性があります。

エラーコールバックをメタファーに変換するには、ハンドラーのreturn errは、「ウィジェットが残っていない場合は、ウィジェットが残っていないことを伝えてください-目的のウィジェットと同じくらい良い」と言うようなものです。

データベースの状況で、db.queryが失敗した場合、最初のerr関数を呼び出します。

…エラーがそこで処理されることを意味します。そうしない場合は、エラーコールバックを省略してください。ところで、成功のコールバックは、それらが作成している約束をreturnしないので、まったく役に立たないようです。正しいでしょう:

var promise = db.query({parent_id: value});
promise.then(function(query_result) {
    var first_value = {
        parent_id: query_result[0].parent_id
    }
    var promise = db.put(first_value);
    return promise.then(function(first_value_result) {
        var second_value = {
            reference_to_first_value_id: first_value_result.id
        }
        var promise = db.put(second_value);
        return promise.then(function(second_value_result) {
            return values_successfully_entered();
        });
    });
});

または、以前のコールバックからの結果値にアクセスするためにクロージャーは必要ないので、次のようになります。

db.query({parent_id: value}).then(function(query_result) {
    return db.put({
        parent_id: query_result[0].parent_id
    });
}).then(function(first_value_result) {
    return db.put({
        reference_to_first_value_id: first_value_result.id
    });
}.then(values_successfully_entered);
26
Bergi

@Jordanは、最初にコメント投稿者が指摘したように、据え置きlibを使用する場合、最初の例が期待どおりの結果を確実に生成します。

promise1 rejected
promise2 rejected
promise3 rejected

次に、提案した出力が生成されても、2番目のスニペットの実行フローには影響しません。

promise.then(function(first_value) {
    console.log('promise1 resolved');
    var promise = db.put(first_value);
    promise.then(function (second_value) {
         console.log('promise2 resolved');
         var promise = db.put(second_value);
         promise.then(
             function (wins) { console.log('promise3 resolved'); },
             function (err) { console.log('promise3 rejected'); return err; });
    }, function (err) { console.log('promise2 rejected'); return err;});
}, function (err) { console.log('promise1 rejected'); return err});

そして、最初の約束が拒否された場合、次のように出力されます:

promise1 rejected

ただし、(最も興味深い部分に到達する)遅延ライブラリは確実に3 x rejectedを返しますが、他のほとんどのpromiseライブラリは1 x rejected, 2 x resolvedを返します(これにより、代わりに他のpromiseライブラリを使用します)。

さらに混乱しているのは、これらの他のライブラリの動作がより正確であることです。説明させてください。

同期の世界では、「約束の拒否」に対応するのはthrowです。つまり、意味的には、非同期のdeferred.reject(new Error())throw new Error()と等しくなります。この例では、同期コールバックでエラーをスローせず、エラーを返すだけなので、成功フローに切り替えて、エラーを成功値とします。拒否が確実に渡されるようにするには、エラーを再スローする必要があります。

function (err) { console.log('promise1 rejected'); throw err; });

だから今問題は、遅延ライブラリがなぜ返されたエラーを拒否として受け取ったのですか?

その理由は、据え置きでの拒否が少し異なる動作をするためです。延期されたlibのルールは次のとおりですエラーのインスタンスで解決されると、約束は拒否されます。したがって、deferred.resolve(new Error())を実行しても、deferred.reject(new Error())として機能し、 deferred.reject(notAnError)を実行しようとすると、エラーが発生した場合にのみプロミスを拒否できるという例外がスローされます。これにより、thenコールバックから返されたエラーがpromiseを拒否する理由が明らかになります。

遅延ロジックの背後にはいくつかの正当な理由がありますが、それでもJavaScriptでのthrowの動作と同等ではなく、そのため、この動作はバージョンv0.7の遅延で変更される予定です。

短い要約:

混乱や予期しない結果を回避するには、適切なルールに従ってください。

  1. 常にエラーインスタンスでプロミスを拒否します(同期世界のルールに従います。エラーではない値をスローすることは悪い習慣と見なされます)。
  2. throwingエラーによって同期コールバックから拒否されます(それらを返すことは拒否を保証するものではありません)。

上記に従って、据え置きライブラリと他の一般的なpromiseライブラリの両方で、一貫した期待される結果が得られます。

1
Mariusz Nowak

使用すると、Promiseの各レベルでエラーをラップできます。 TraceErrorでエラーを連鎖させました:

class TraceError extends Error {
  constructor(message, ...causes) {
    super(message);

    const stack = Object.getOwnPropertyDescriptor(this, 'stack');

    Object.defineProperty(this, 'stack', {
      get: () => {
        const stacktrace = stack.get.call(this);
        let causeStacktrace = '';

        for (const cause of causes) {
          if (cause.sourceStack) { // trigger lookup
            causeStacktrace += `\n${cause.sourceStack}`;
          } else if (cause instanceof Error) {
            causeStacktrace += `\n${cause.stack}`;
          } else {
            try {
              const json = JSON.stringify(cause, null, 2);
              causeStacktrace += `\n${json.split('\n').join('\n    ')}`;
            } catch (e) {
              causeStacktrace += `\n${cause}`;
              // ignore
            }
          }
        }

        causeStacktrace = causeStacktrace.split('\n').join('\n    ');

        return stacktrace + causeStacktrace;
      }
    });

    // access first error
    Object.defineProperty(this, 'cause', {value: () => causes[0], enumerable: false, writable: false});

    // untested; access cause stack with error.causes()
    Object.defineProperty(this, 'causes', {value: () => causes, enumerable: false, writable: false});
  }
}

使用方法

throw new TraceError('Could not set status', srcError, ...otherErrors);

出力

関数

TraceError#cause - first error
TraceError#causes - list of chained errors
0
Mathew Kurian