打破承诺链并根据链中被破坏(拒绝)的步骤调用函数

Posted

技术标签:

【中文标题】打破承诺链并根据链中被破坏(拒绝)的步骤调用函数【英文标题】:Break promise chain and call a function based on the step in the chain where it is broken (rejected) 【发布时间】:2014-01-09 22:55:54 【问题描述】:

更新:

为了帮助这篇文章的未来观众,我创建了 this demo of pluma's answer。

问题:

我的目标似乎相当简单。

  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) 会,我理解。除了我想做的事情,我已经完成了所有事情。

如何编写 Promise 以便在拒绝时调用函数,而不调用错误链中的所有函数?还是有其他方法可以做到这一点?

Here's a live demo 所以你有一些工作要做。

更新:

有点已经解决了。在这里,我在链的末尾捕获错误并将数据传递给reject(data),以便我知道要在错误函数中处理什么问题。这实际上不符合我的要求,因为我不想依赖数据。这会很蹩脚,但在我的情况下,将错误回调传递给函数会更干净,而不是依赖返回的数据来确定要做什么。

Live demo here (click).

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); 
  

【问题讨论】:

有一个异步 javascript 库,如果这变得更复杂,可能会有所帮助 Promise.prototype.catch() examples on MDN 显示完全相同问题的解决方案。 【参考方案1】:

如果您在任何时候返回 Promise.reject('something'),您将被扔到承诺的 catch 块中。

promiseOne
  .then((result) => 
    if (!result) 
      return Promise.reject('No result');
    
    return;
  )
  .catch((err) => 
    console.log(err);
  );

如果第一个 Promise 没有返回任何结果,您只会在控制台中得到 'No result'

【讨论】:

【参考方案2】:

使用 SequentialPromise 模块

意图

提供一个模块,其职责是顺序执行请求,同时以有序的方式跟踪每个操作的当前索引。在命令模式中定义操作以获得灵活性。

参与者

上下文:其成员方法执行操作的对象。 SequentialPromise:定义一个execute 方法来链接和跟踪每个操作。 SequentialPromise 从所有执行的操作中返回一个 Promise-Chain。 Invoker:创建一个 SequentialPromise 实例,为其提供上下文和操作,并调用其 execute 方法,同时为每个操作传入一个顺序选项列表。

后果

当需要 Promise 解析的序数行为时使用 SequentialPromise。 SequentialPromise 将跟踪 Promise 被拒绝的索引。

实施

clear();

var http = 
    get(url) 
        var delay = Math.floor( Math.random() * 10 ), even = !(delay % 2);
        var xhr = new Promise(exe);

        console.log(`REQUEST`, url, delay);
        xhr.then( (data) => console.log(`SUCCESS: `, data) ).catch( (data) => console.log(`FAILURE: `, data) );

        function exe(resolve, reject) 
            var action =  'true': reject, 'false': resolve [ even ];
            setTimeout( () => action( url, delay ), (1000 * delay) );
        

        return xhr;
    
;

var SequentialPromise = new (function SequentialPromise() 
    var PRIVATE = this;

    return class SequentialPromise 

        constructor(context, action) 
            this.index = 0;
            this.requests = [ ];
            this.context = context;
            this.action = action;

            return this;
        

        log() 

        execute(url, ...more) 
            var  context, action, requests  = this;
            var chain = context[action](url);

            requests.push(chain);
            chain.then( (data) => this.index += 1 );

            if (more.length) return chain.then( () => this.execute(...more) );
            return chain;
        

    ;
)();

var sequence = new SequentialPromise(http, 'get');
var urls = [
    'url/name/space/0',
    'url/name/space/1',
    'url/name/space/2',
    'url/name/space/3',
    'url/name/space/4',
    'url/name/space/5',
    'url/name/space/6',
    'url/name/space/7',
    'url/name/space/8',
    'url/name/space/9'
];
var chain = sequence.execute(...urls);
var promises = sequence.requests;

chain.catch( () => console.warn(`EXECUTION STOPPED at $sequence.index for $urls[sequence.index]`) );

// console.log('>', chain, promises);

要点

SequentialPromise

【讨论】:

【参考方案3】:

如果你想使用 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 []
    

)()

【讨论】:

【参考方案4】:

尝试像库一样使用它:

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);
);

【讨论】:

【参考方案5】:

最好的解决方案是重构您的承诺链以使用 ES6 等待。然后你可以从函数中返回以跳过其余的行为。

一年多来我一直在反对这种模式,并且使用 await 是天堂。

【讨论】:

当使用纯 IE 时不支持 async/await。【参考方案6】:

发现下面的Promise.prototype.catch() examples on MDN 很有帮助。

(接受的答案提到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);
);

【讨论】:

【参考方案7】:

参加聚会有点晚了,但这个简单的解决方案对我有用:

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

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

这可以让你打破链。

【讨论】:

帮助了我,但仅供参考,您可以在随后将其退回以在捕获中突围,例如:.then(user => if (user) return Promise.reject('The email address already exists.') ) @CraigvanTonder 你可以在一个承诺中抛出,它会和你的代码一样工作:.then(user => if (user) throw 'The email address already exists.' ) 这是唯一正确的答案。否则即使步骤 1 出错,步骤 3 仍会执行。 澄清一下,如果 stepOne() 中发生错误,那么两个 chainError 都会被调用,对吗?如果这是可取的。我有一个可以做到这一点的 sn-p,不确定我是否误解了任何东西 - runkit.com/embed/9q2q3rjxdar9【参考方案8】:

拒绝时,您应该传递拒绝错误,然后将步骤错误处理程序包装在一个函数中,该函数检查是否应处理拒绝或“重新抛出”直到链结束:

// 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);
);

【讨论】:

【参考方案9】:

将错误处理程序作为单独的链元素直接附加到步骤的执行中:

        // 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 suggests in his answer 基本相同,但使用了 OP 的命名。

【讨论】:

【参考方案10】:

您的代码无法按预期工作的原因是它实际上做了一些与您认为的不同的事情。

假设您有以下内容:

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

为了更好地理解发生了什么,让我们假设这是与 try/catch 块的同步代码:

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);

onRejected 处理程序(then 的第二个参数)本质上是一种纠错机制(如catch 块)。如果在handleErrorOne 中抛出错误,它将被下一个catch 块(catch(e2))捕获,以此类推。

这显然不是你想要的。

假设我们希望整个解析链无论出现什么问题都失败:

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

注意:我们可以将handleErrorOne留在原处,因为它只有在stepOne拒绝时才会被调用(它是链中的第一个函数,所以我们知道如果此时链被拒绝,它只能是因为那个函数的承诺)。

重要的变化是其他函数的错误处理程序不是主要承诺链的一部分。相反,每个步骤都有自己的“子链”,带有onRejected,只有在步骤被拒绝时才会调用(但主链无法直接访问)。

之所以有效,是因为onFulfilledonRejected 都是then 方法的可选参数。如果一个承诺被履行(即解决)并且链中的下一个then 没有onFulfilled 处理程序,则链将继续,直到有一个具有这样的处理程序。

这意味着以下两行是等价的:

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

但下面这行等同于上面两行:

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

Angular 的 promise 库 $q 基于 kriskowal 的 Q 库(它具有更丰富的 API,但包含您可以在 $q 中找到的所有内容)。 Q 在 GitHub 上的 API docs 可能很有用。 Q 实现了Promises/A+ spec,它详细介绍了then 和promise 解析行为是如何工作的。

编辑:

还要记住,如果你想在你的错误处理程序中跳出链,它需要返回一个被拒绝的承诺或抛出一个错误(这将被自动捕获并包装在一个被拒绝的承诺中)。如果您不返回承诺,then 会将返回值包装在为您解决的承诺中。

这意味着,如果您不返回任何内容,您实际上是在返回值 undefined 的已解决承诺。

【讨论】:

这部分是金子:if you don't return anything, you are effectively returning a resolved promise for the value undefined. 谢谢@pluma 确实如此。我正在对其进行编辑以使其具有应有的粗体 是否拒绝退出当前功能?例如,如果第一次调用reject,则不会调用resolve ` if (bad) reject(status); 解决(结果);` stepOne().then(stepTwo, handleErrorOne) ` stepOne().then(null, handleErrorOne).then(stepTwo)` 这些真的等价吗?我认为如果stepOne 被拒绝,第二行代码将执行stepTwo,但第一行只会执行handleErrorOne 并停止。还是我错过了什么? 并没有真正为所提出的问题提供明确的解决方案,但很好的解释【参考方案11】:
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

【讨论】:

但如果我打电话给deferred.reject(n),那么我会收到警告,承诺被非错误对象拒绝【参考方案12】:

您需要的是一个重复的 .then() 链,其中包含一个开始的特殊情况和一个结束的特殊情况。

诀窍是获取失败案例的步骤号以传递到最终的错误处理程序。

开始:无条件拨打step(1)。 重复模式:使用以下回调链接.then(): 成功:调用步骤(n+1) failure:抛出前一个 deferred 被拒绝的值或重新抛出错误。 完成:链接.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););

见demo

请注意在step() 中,延迟是如何被拒绝或使用n 解决的,从而使该值可用于链中下一个.then() 中的回调。一旦调用了stepError,就会重复抛出错误,直到被finalError处理。

【讨论】:

信息丰富的答案,因此值得保留,但这不是我面临的问题。我在我的帖子中提到了这个解决方案,这不是我想要的。请参阅我帖子顶部的演示。 m59,这是对所提问题的回答,“我如何编写 Promise 以便在拒绝时调用函数,而不调用错误链中的所有函数?”以及问题的标题,“打破承诺链并根据链中被破坏(拒绝)的步骤调用函数” 对,就像我说的,它提供了丰富的信息,我什至在我的帖子中包含了这个解决方案(细节较少)。这种方法旨在修复事物,以便链条可以继续。虽然它可以完成我正在寻找的东西,但它不像公认答案中的方法那么自然。换句话说,如果你想做标题和问题所表达的事情,请采取 pluma 的方法。【参考方案13】:

如果我理解正确,您只想显示失败步骤的错误,对吗?

这应该就像将第一个承诺的失败案例更改为这样简单:

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

通过在第一步失败的情况下返回 $q.reject(),您将拒绝该承诺,这会导致在第二个 then(...) 中调用 errorCallback。

【讨论】:

这到底是什么……这正是我所做的!在我的帖子中看到我确实尝试过,但链条会重新启动并运行step(2)。现在我只是再次尝试它没有发生。我很困惑。 我确实看到你提到了这一点。不过这很奇怪。包含return step(2); 的函数只应在step(1) 成功解析时调用。 从头开始 - 它肯定正在发生。就像我在帖子中所说的那样,如果你不使用return $q.reject(),链条将继续运行。在这种情况下,return response 搞砸了。看到这个:jsbin.com/EpaZIsIp/6/edit 嗯,好的。当我改变它时,它似乎在你发布的 jsbin 中工作,但我一定错过了一些东西。 是的,我确实看到它现在不起作用。回到我的绘图板上!

以上是关于打破承诺链并根据链中被破坏(拒绝)的步骤调用函数的主要内容,如果未能解决你的问题,请参考以下文章

扩展 Javascript 承诺并在构造函数中解决或拒绝它

复杂承诺返回链中的承诺 catch() 顺序

打破承诺链 - 停止执行下一个“then”

拒绝后承诺链继续

如何编写测试用例来覆盖承诺链中所有嵌套的“then”回调

蓝鸟承诺 - 嵌套与拒绝模式