web-dev-qa-db-ja.com

promiseチェーンを解除し、チェーン内の壊れた(拒否された)ステップに基づいて関数を呼び出す

更新:

この投稿の今後の視聴者を支援するために、プラマの答えのこのデモを作成しました

質問:

私の目標はかなり簡単なようです。

  step(1)
  .then(function() {
    return step(2);
  }, function() {
    stepError(1);
    return $q.reject();
  })
  .then(function() {

  }, function() {
    stepError(2);
  });

  function step(n) {
    var deferred = $q.defer();
    //fail on step 1
    (n === 1) ? deferred.reject() : deferred.resolve();
    return deferred.promise;
  }
  function stepError(n) {
    console.log(n); 
  }

ここでの問題は、ステップ1で失敗すると、stepError(1)stepError(2)の両方が起動されることです。 return $q.rejectを実行しないと、stepError(2)は実行されませんが、step(2)は実行されます。私がやろうとしていること以外のすべてを達成しました。

エラーチェーン内のすべての関数を呼び出さずに、拒否時に関数を呼び出すことができるように、どうすればプロミスを書くことができますか?または、これを達成する別の方法はありますか?

ここにライブデモがあります .

更新:

種類のはそれを解決しました。ここでは、チェーンの最後でエラーをキャッチし、データをreject(data)に渡しているため、エラー関数で処理する問題を把握できます。データに依存したくないので、これは実際に私の要件を満たしていません。足りないかもしれませんが、私の場合は、返されたデータに依存して何をすべきかを判断するのではなく、関数にエラーコールバックを渡す方がきれいです。

ライブデモはこちら(クリック)

step(1)
  .then(function() {
    return step(2);
  })
  .then(function() {
    return step(3);
  })
  .then(false, 
    function(x) {
      stepError(x);
    }
  );
  function step(n) {
    console.log('Step '+n);
    var deferred = $q.defer();
    (n === 1) ? deferred.reject(n) : deferred.resolve(n);
    return deferred.promise;
  }
  function stepError(n) {
    console.log('Error '+n); 
  }
116
m59

コードが期待どおりに機能しないのは、実際にあなたが思っているのとは異なることをしているからです。

次のようなものがあるとしましょう:

stepOne()
.then(stepTwo, handleErrorOne)
.then(stepThree, handleErrorTwo)
.then(null, handleErrorThree);

何が起こっているのかをよりよく理解するために、これがtryname __/catchname__ブロックを持つ同期コードであると仮定しましょう:

try {
    try {
        try {
            var a = stepOne();
        } catch(e1) {
            a = handleErrorOne(e1);
        }
        var b = stepTwo(a);
    } catch(e2) {
        b = handleErrorTwo(e2);
    }
    var c = stepThree(b);
} catch(e3) {
    c = handleErrorThree(e3);
}

onRejectedname__ハンドラー(thenname__の2番目の引数)は、本質的に(catchname__ブロックのような)エラー修正メカニズムです。 handleErrorOnename__でエラーがスローされると、次のcatchブロック(catch(e2))などでキャッチされます。

これは明らかに意図したものではありません。

何がうまくいかなくても、解決チェーン全体が失敗するようにしたいとしましょう。

stepOne()
.then(function(a) {
    return stepTwo(a).then(null, handleErrorTwo);
}, handleErrorOne)
.then(function(b) {
    return stepThree(b).then(null, handleErrorThree);
});

注:handleErrorOnename__は、stepOnename__が拒否された場合にのみ呼び出されるため、onRejectedname__をそのままにしておくことができます(チェーンの最初の関数です。したがって、この時点でチェーンが拒否された場合、約束する)。

重要な変更は、他の関数のエラーハンドラーがメインのpromiseチェーンの一部ではないことです。代わりに、各ステップには、ステップが拒否された場合にのみ呼び出される__main_variable_name__を持つ独自の「サブチェーン」があります(ただし、メインチェーンから直接到達することはできません)。

これが機能する理由は、onFulfilledname__とonRejectedname__の両方がthenname__メソッドのオプションの引数であるためです。約束が満たされた場合(つまり、解決された場合)、チェーン内の次のthenname__にonFulfilledname__ハンドラーがない場合、チェーンはそのようなハンドラーを持つものが存在するまで継続します。

つまり、次の2行は同等です。

stepOne().then(stepTwo, handleErrorOne)
stepOne().then(null, handleErrorOne).then(stepTwo)

ただし、次の行は、上記の2つとnotに相当します。

stepOne().then(stepTwo).then(null, handleErrorOne)

Angularのpromiseライブラリ$qは、kriskowalのQname__ライブラリ(より豊富なAPIを備えていますが、$qにあるすべてのものが含まれています)に基づいています。 Q's API docs GitHubの有用性を証明できます。 Qは Promises/A + spec を実装します。これは、thenname__およびpromise解決動作が正確に機能する方法について詳しく説明します。

編集:

また、エラーハンドラでチェーンから抜け出すには、拒否されたプロミスを返すか、エラーをスローする必要があることに注意してください(エラーはキャッチされ、拒否されたプロミスに自動的にラップされます)。 promiseを返さない場合、thenname__は戻り値を解決プロミスでラップします。

これは、何も返さない場合、値undefinedname__の解決されたプロミスを効果的に返すことを意味します。

175
Alan Plum

パーティーに少し遅れましたが、この簡単な解決策はうまくいきました:

function chainError(err) {
  return Promise.reject(err)
};

stepOne()
.then(stepTwo, chainError)
.then(stepThreee, chainError);

これにより、チェーンの外でbreakが可能になります。

46
Vinnyq12

必要なのは、開始する特別なケースと終了する特別なケースを持つ繰り返しの.then()チェーンです。

コツは、失敗事例のステップ番号を取得して最終的なエラーハンドラーに波及させることです。

  • 開始:step(1)を無条件に呼び出します。
  • 繰り返しパターン:.then()を次のコールバックでチェーンします:
    • 成功:step(n + 1)を呼び出します
    • 失敗:前の延期が拒否された値をスローするか、エラーを再スローします。
  • 終了:成功ハンドラーと最終エラーハンドラーなしで.then()をチェーンします。

すべてを手書きで書くこともできますが、名前付きの一般化された関数を使用してパターンを示す方が簡単です:

function NeXTSTEP(n) {
    return step(n + 1);
}

function step(n) {
    console.log('step ' + n);
    var deferred = $q.defer();
    (n === 3) ? deferred.reject(n) : deferred.resolve(n);
    return deferred.promise;
}

function stepError(n) {
    throw(n);
}

function finalError(n) {
    console.log('finalError ' + n);
}
step(1)
    .then(NeXTSTEP, stepError)
    .then(NeXTSTEP, stepError)
    .then(NeXTSTEP, stepError)
    .then(NeXTSTEP, stepError)
    .then(NeXTSTEP, stepError)
    .then(null, finalError);});

デモ を参照

step()では、遅延変数がnで拒否または解決され、チェーン内の次の.then()でコールバックがその値を利用できるようになることに注意してください。 stepErrorが呼び出されると、エラーはfinalErrorによって処理されるまで繰り返しスローされます。

9

拒否する場合は、拒否エラーを渡す必要があります。次に、拒否を処理するか、チェーンの終わりまで「再スロー」するかをチェックする関数でステップエラーハンドラをラップします。

// function mocking steps
function step(i) {
    i++;
    console.log('step', i);
    return q.resolve(i);
}

// function mocking a failing step
function failingStep(i) {
    i++;
    console.log('step '+ i + ' (will fail)');
    var e = new Error('Failed on step ' + i);
    e.step = i;
    return q.reject(e);
}

// error handler
function handleError(e){
    if (error.breakChain) {
        // handleError has already been called on this error
        // (see code bellow)
        log('errorHandler: skip handling');
        return q.reject(error);
    }
    // firs time this error is past to the handler
    console.error('errorHandler: caught error ' + error.message);
    // process the error 
    // ...
    //
    error.breakChain = true;
    return q.reject(error);
}

// run the steps, will fail on step 4
// and not run step 5 and 6
// note that handleError of step 5 will be called
// but since we use that error.breakChain boolean
// no processing will happen and the error will
// continue through the rejection path until done(,)

  step(0) // 1
  .catch(handleError)
  .then(step) // 2
  .catch(handleError)
  .then(step) // 3
  .catch(handleError)
  .then(failingStep)  // 4 fail
  .catch(handleError)
  .then(step) // 5
  .catch(handleError)
  .then(step) // 6
  .catch(handleError)
  .done(function(){
      log('success arguments', arguments);
  }, function (error) {
      log('Done, chain broke at step ' + error.step);
  });

コンソールに表示されるもの:

step 1
step 2
step 3
step 4 (will fail)
errorHandler: caught error 'Failed on step 4'
errorHandler: skip handling
errorHandler: skip handling
Done, chain broke at step 4

ここにいくつかの作業コードがあります https://jsfiddle.net/8hzg5s7m/3/

各ステップに特定の処理がある場合、ラッパーは次のようになります。

/*
 * simple wrapper to check if rejection
 * has already been handled
 * @param function real error handler
 */
function createHandler(realHandler) {
    return function(error) {
        if (error.breakChain) {
            return q.reject(error);
        }
        realHandler(error);
        error.breakChain = true;
        return q.reject(error);    
    }
}

あなたのチェーン

step1()
.catch(createHandler(handleError1Fn))
.then(step2)
.catch(createHandler(handleError2Fn))
.then(step3)
.catch(createHandler(handleError3Fn))
.done(function(){
    log('success');
}, function (error) {
    log('Done, chain broke at step ' + error.step);
});
6
redben

私が正しく理解していれば、失敗したステップのエラーのみを表示したいでしょうか?

これは、最初の約束の失敗ケースをこれに変更するのと同じくらい簡単です:

step(1).then(function (response) {
    step(2);
}, function (response) {
    stepError(1);
    return response;
}).then( ... )

最初のステップの失敗の場合に$q.reject()を返すことにより、そのプロミスを拒否し、2番目のthen(...)でerrorCallbackが呼び出されます。

2
Zajn
var s = 1;
start()
.then(function(){
    return step(s++);
})
.then(function() {
    return step(s++);
})
.then(function() {
    return step(s++);
})
.then(0, function(e){
   console.log(s-1); 
});

http://jsbin.com/EpaZIsIp/20/edit

または、任意の数のステップで自動化:

var promise = start();
var s = 1;
var l = 3;
while(l--) {
    promise = promise.then(function() {
        return step(s++);
    });
}
promise.then(0, function(e){
   console.log(s-1); 
});

http://jsbin.com/EpaZIsIp/21/edit

2
Esailija

エラーハンドラーを個別のチェーン要素として直接ステップの実行にアタッチします。

        // Handle errors for step(1)
step(1).then(null, function() { stepError(1); return $q.reject(); })
.then(function() {
                 // Attach error handler for step(2),
                 // but only if step(2) is actually executed
  return step(2).then(null, function() { stepError(2); return $q.reject(); });
})
.then(function() {
                 // Attach error handler for step(3),
                 // but only if step(3) is actually executed
  return step(3).then(null, function() { stepError(3); return $q.reject(); });
});

またはcatch()を使用:

       // Handle errors for step(1)
step(1).catch(function() { stepError(1); return $q.reject(); })
.then(function() {
                 // Attach error handler for step(2),
                 // but only if step(2) is actually executed
  return step(2).catch(function() { stepError(2); return $q.reject(); });
})
.then(function() {
                 // Attach error handler for step(3),
                 // but only if step(3) is actually executed
  return step(3).catch(function() { stepError(3); return $q.reject(); });
});

注:これは基本的に plumaが答えで示唆している と同じパターンですが、OPの命名を使用しています。

1
Ignitor

MDNの Promise.prototype.catch()の例 が非常に役立ちました。

(受け入れられた答えはthen(null, onErrorHandler)に言及していますが、これは基本的にcatch(onErrorHandler)と同じです。)

Catchメソッドの使用と連鎖

var p1 = new Promise(function(resolve, reject) {
  resolve('Success');
});

p1.then(function(value) {
  console.log(value); // "Success!"
  throw 'oh, no!';
}).catch(function(e) {
  console.log(e); // "oh, no!"
}).then(function(){
  console.log('after a catch the chain is restored');
}, function () {
  console.log('Not fired due to the catch');
});

// The following behaves the same as above
p1.then(function(value) {
  console.log(value); // "Success!"
  return Promise.reject('oh, no!');
}).catch(function(e) {
  console.log(e); // "oh, no!"
}).then(function(){
  console.log('after a catch the chain is restored');
}, function () {
  console.log('Not fired due to the catch');
});

エラーを投げるときの落とし穴

// Throwing an error will call the catch method most of the time
var p1 = new Promise(function(resolve, reject) {
  throw 'Uh-oh!';
});

p1.catch(function(e) {
  console.log(e); // "Uh-oh!"
});

// Errors thrown inside asynchronous functions will act like uncaught errors
var p2 = new Promise(function(resolve, reject) {
  setTimeout(function() {
    throw 'Uncaught Exception!';
  }, 1000);
});

p2.catch(function(e) {
  console.log(e); // This is never called
});

// Errors thrown after resolve is called will be silenced
var p3 = new Promise(function(resolve, reject) {
  resolve();
  throw 'Silenced Exception!';
});

p3.catch(function(e) {
   console.log(e); // This is never called
});

解決したら

//Create a promise which would not call onReject
var p1 = Promise.resolve("calling next");

var p2 = p1.catch(function (reason) {
    //This is never called
    console.log("catch p1!");
    console.log(reason);
});

p2.then(function (value) {
    console.log("next promise's onFulfilled"); /* next promise's onFulfilled */
    console.log(value); /* calling next */
}, function (reason) {
    console.log("next promise's onRejected");
    console.log(reason);
});
1
toraritte

Async/awaitを使用してこの問題を解決する場合:

(async function(){    
    try {        
        const response1, response2, response3
        response1 = await promise1()

        if(response1){
            response2 = await promise2()
        }
        if(response2){
            response3 = await promise3()
        }
        return [response1, response2, response3]
    } catch (error) {
        return []
    }

})()
1
luispa

最善の解決策は、ES6 awaitを使用するようにプロミスチェーンにリファクタリングすることです。その後、関数から戻って、残りの動作をスキップできます。

私はこのパターンに一年以上頭を打ち続けており、awaitの使用は天国です。

1
Pete Alvin

これをlibsのように使用してみてください:

https://www.npmjs.com/package/promise-chain-break

    db.getData()
.then(pb((data) => {
    if (!data.someCheck()) {
        tellSomeone();

        // All other '.then' calls will be skiped
        return pb.BREAK;
    }
}))
.then(pb(() => {
}))
.then(pb(() => {
}))
.catch((error) => {
    console.error(error);
});
1
Leonid