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

时间:2013-12-21 01:30:59

标签: javascript angularjs promise

更新

为了帮助此帖子的未来观看者,我创建了 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)将不会被解雇,但{​​{1}}将被解雇,我理解。除了我想要做的事情之外,我已经完成了所有的事情。

如何编写promises以便我可以在拒绝时调用函数,而无需调用错误链中的所有函数?或者还有另一种方法可以实现这一目标吗?

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

更新

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

Live demo here (click).

reject(data)

13 个答案:

答案 0 :(得分:173)

你的代码没有按预期工作的原因是它实际上做的与你的想法不同。

假设你有以下内容:

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方法的可选参数。如果一个promise已经完成(即已解决)并且链中的下一个then没有onFulfilled处理程序,则该链将继续,直到有一个具有这样的处理程序。

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

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

但是以下行等同于上面两个:

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

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

编辑:

另外请记住,如果你想在错误处理程序中突破链,它需要返回一个被拒绝的promise或者抛出一个错误(它将被捕获并自动包含在被拒绝的promise中)。如果您未返回承诺,then将返回值包装在您的解决承诺中。

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

答案 1 :(得分:46)

派对迟到了,但这个简单的解决方案对我有用:

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

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

这允许你打破链。

答案 2 :(得分:9)

你需要的是一个重复的.then()链,有一个特殊的案例可以开始,还有一个特殊的案例要完成。

诀窍是让失败案例的步骤编号连接到最终的错误处理程序。

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

请参阅demo

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

答案 3 :(得分:6)

当拒绝时,您应该传递拒绝错误,然后将步骤错误处理程序包装在一个函数中,该函数检查是否应该处理拒绝或者#t; rethrown"直到链的末端:

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

答案 4 :(得分:2)

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

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

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

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

答案 5 :(得分: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

答案 6 :(得分: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 []
    }

})()

答案 7 :(得分: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 suggests in his answer的模式基本相同,但使用OP的命名。

答案 8 :(得分:1)

在下面找到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);
});

答案 9 :(得分:1)

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

答案 10 :(得分:0)

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

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

答案 11 :(得分:0)

使用SequentialPromise模块

意图

提供一个模块,该模块负责按顺序执行请求,同时按顺序跟踪每个操作的当前索引。为了灵活,请在命令模式中定义操作。

参与者

  • 上下文:其成员方法执行操作的对象。
  • SequentialPromise :定义一个execute方法来链接和跟踪每个操作。 SequentialPromise从执行的所有操作中返回一个Promise-Chain。
  • 调用者:创建一个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

答案 12 :(得分:0)

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

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

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