中断promise链,并根据该链中被破坏(被拒绝)的步骤调用函数


135

更新:

为了帮助将来的读者,我创建了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)AND都会stepError(2)被触发。如果我不return $q.reject那么stepError(2)就不会被解雇,但是step(2)会,我明白了。除了我想做的以外,我已经完成了所有工作。

如何编写promise,以便我可以在拒绝时调用函数,而无需调用错误链中的所有函数?还是有另一种方法可以做到这一点?

这是现场演示因此您可以进行一些工作。

更新:

有点解决了。在这里,我在链的末尾捕获错误,并将数据传递给reject(data)我,以便我知道要在错误函数中处理什么问题。这实际上不符合我的要求,因为我不想依赖数据。会很la脚,但就我而言,将错误回调传递给函数将比使用返回的数据来确定执行操作更干净。

现场演示在这里(单击)。

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

1
如果这变得更加复杂,则有一个异步javascript库可能会
有所

Promise.prototype.catch()MDN上的示例显示了完全相同的问题的解决方案。
toraritte 18/09/11

Answers:


199

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

假设您有类似以下内容的东西:

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拒绝时才会调用它(它是链中的第一个函数,因此我们知道如果链在此刻被拒绝,那只能是由于该函数的诺言而已) 。

重要的变化是其他功能的错误处理程序不属于主要的Promise链。相反,每个步骤都有其自己的“子链”,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)。GitHub上的Q的API文档可能被证明是有用的。Q实施Promises / A +规范,其中详细介绍了如何then以及保证承诺行为。

编辑:

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

这意味着,如果您不返回任何内容,则实际上是在返回一个已解析的value承诺undefined


138
这部分是金子:if you don't return anything, you are effectively returning a resolved promise for the value undefined.谢谢@pluma
Valerio

7
的确如此。我正在对其进行编辑,以使其应有的粗体
Cyril CHAPON 2015年

拒绝退出当前功能吗?例如,如果拒绝被称为1st,则不会调用resolve。if(bad){reject(status); } resolve(results);`
SuperUberDuper 2015年

stepOne().then(stepTwo, handleErrorOne) `stepOne()。then(null,handleErrorOne).then(stepTwo)`这些是真的吗?我认为在stepOne第二行代码被拒绝的情况下会执行,stepTwo但第一行代码只会执行handleErrorOne并停止。还是我错过了什么?
JeFf

5
并未真正为提出的问题提供清晰的解决方案,但是仍然提供了很好的解释
Yerken

57

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

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

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

这可以让你打破链出。


1
仅供参考,对我有帮助,但是您可以将其归还给您,例如:.then(user => { if (user) return Promise.reject('The email address already exists.') })
Craig van Tonder,2017年

1
@CraigvanTonder,您可以在承诺范围内投入,它将与您的代码相同:.then(user => { if (user) throw 'The email address already exists.' })
Francisco Presencia

1
这是唯一正确的答案。否则,即使第1步有错误,第3步仍将执行。
wdetac'2

1
只是为了澄清一下,如果在stepOne()中发生错误,那么两个chainError都会被调用正确吗?如果这可取。我有一个代码片段可以执行此操作,不确定我是否误解了什么-runkit.com/embed/9q2q3rjxdar9
user320550

10

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

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

  • 开始:step(1)无条件呼叫。
  • 重复模式:.then()使用以下回调链接a :
    • 成功:呼叫步骤(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);});

观看演示

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


内容翔实的答案,所以值得保留,但这不是我面临的问题。我在帖子中提到了这种解决方案,但这不是我想要的。请参阅我的文章顶部的演示。
2013年

1
m59,这是对以下问题的回答:“我如何编写承诺,以便我可以在拒绝时调用函数,而无需调用错误链中的所有函数?” 和问题的标题,“打破承诺链并根据打破(拒绝)的链中的步骤调用函数”
Beetroot-Beetroot

是的,就像我说的那样,它提供了很多信息,我什至在我的帖子中都包含了该解决方案(细节较少)。此方法旨在修复问题,以便链可以继续。虽然可以完成我要寻找的内容,但它不如接受的答案中的方法那么自然。换句话说,如果您想做标题和所问问题所表示的操作,请采取pluma的方法。
2013年

7

拒绝时,您应该传递拒绝错误,然后将步错误处理程序包装到一个函数中,该函数检查是否应该处理拒绝或“拒绝”直到链的末端:

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

2

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

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

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

通过返回$q.reject()第一步的失败案例,您将拒绝该诺言,这将导致在2nd中调用errorCallback then(...)


到底是什么...那正是我所做的!在我的帖子中看到我确实尝试过,但是链条会重新插入并运行step(2)。现在,我只是再次尝试了,却没有发生。我很困惑。
13年

1
我确实看到你提到过。不过那很奇怪。return step(2);只有step(1)成功解析后,才应调用包含该函数的函数。
Zajn 2013年

从头开始-肯定会发生。就像我在帖子中所说的那样,如果您不使用return $q.reject(),链条将会继续前进。在这种情况下,return response它搞砸了。看到这个:jsbin.com/EpaZIsIp/6/edit
m59 2013年

嗯好吧 当我更改它时,它似乎可以在您发布的jsbin中使用,但是我一定错过了一些东西。
Zajn

是的,我绝对看到现在不行。回到我的画板!
Zajn

2
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)我,那么我会得到警告,承诺被nonError对象拒绝了
9me

2

尝试ro像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);
});

2

如果要使用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

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

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

Promise.prototype.catch()下面找到有关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);
});

1

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

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


使用纯IE时,不支持异步/等待。
ndee '19

0

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

要旨

顺序承诺


0

如果您在任何时候回来,Promise.reject('something')您都会被丢到承诺的陷阱区。

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

如果第一个承诺未返回任何结果,则您在控制台中只会得到“无结果”

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.