Break promise chain and call a function based on t

2019-01-03 08:03发布

Update:

To help future viewers of this post, I created this demo of pluma's answer.

Question:

My goal seems fairly straightforward.

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

The problem here is that if I fail on step 1, both stepError(1) AND stepError(2) are fired. If I don't return $q.reject then stepError(2) won't be fired, but step(2) will, which I understand. I've accomplished everything except what I'm trying to do.

How do I write promises so that I can call a function on rejection, without calling all of the functions in the error chain? Or is there another way to accomplish this?

Here's a live demo so you've got something work with.

Update:

I kind of have solved it. Here, I am catching the error at the end of the chain and passing the data to reject(data) so that I will know what issue to handle in the error function. This actually doesn't meet my requirements because I don't want to depend on the data. It would be lame, but in my case it would be cleaner to pass an error callback to the function rather than to depend on the returned data to determine what to do.

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

9条回答
唯我独甜
2楼-- · 2019-01-03 08:27

If I understand correctly, you want only the error for the failing step to show, right?

That should be as simple as changing the failure case of the first promise to this:

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

By returning $q.reject() in the first step's failure case, you're rejecting that promise, which causes the errorCallback to be called in the 2nd then(...).

查看更多
Viruses.
3楼-- · 2019-01-03 08:31

The reason your code doesn't work as expected is that it's actually doing something different from what you think it does.

Let's say you have something like the following:

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

To better understand what's happening, let's pretend this is synchronous code with try/catch blocks:

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

The onRejected handler (the second argument of then) is essentially an error correction mechanism (like a catch block). If an error is thrown in handleErrorOne, it will be caught by the next catch block (catch(e2)), and so on.

This is obviously not what you intended.

Let's say we want the entire resolution chain to fail no matter what goes wrong:

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

Note: We can leave the handleErrorOne where it is, because it will only be invoked if stepOne rejects (it's the first function in the chain, so we know that if the chain is rejected at this point, it can only be because of that function's promise).

The important change is that the error handlers for the other functions are not part of the main promise chain. Instead, each step has its own "sub-chain" with an onRejected that is only called if the step was rejected (but can not be reached by the main chain directly).

The reason this works is that both onFulfilled and onRejected are optional arguments to the then method. If a promise is fulfilled (i.e. resolved) and the next then in the chain doesn't have an onFulfilled handler, the chain will continue until there is one with such a handler.

This means the following two lines are equivalent:

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

But the following line is not equivalent to the two above:

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

Angular's promise library $q is based on kriskowal's Q library (which has a richer API, but contains everything you can find in $q). Q's API docs on GitHub could prove useful. Q implements the Promises/A+ spec, which goes into detail on how then and the promise resolution behaviour works exactly.

EDIT:

Also keep in mind that if you want to break out of the chain in your error handler, it needs to return a rejected promise or throw an Error (which will be caught and wrapped in a rejected promise automatically). If you don't return a promise, then wraps the return value in a resolve promise for you.

This means that if you don't return anything, you are effectively returning a resolved promise for the value undefined.

查看更多
爷、活的狠高调
4楼-- · 2019-01-03 08:33
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

Or automated for any number of steps:

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

查看更多
成全新的幸福
5楼-- · 2019-01-03 08:36

When rejecting you should pass an rejection error, then wrap step error handlers in a function that checks whether the rejection should be processed or "rethrown" until the end of the chain :

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

What you'd see on the console :

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

Here is some working code https://jsfiddle.net/8hzg5s7m/3/

If you have specific handling for each step, your wrapper could be something like:

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

then your chain

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);
});
查看更多
We Are One
6楼-- · 2019-01-03 08:37

Attach error handlers as separate chain elements directly to the execution of the steps:

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

or using 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(); });
});

Note: This is basically the same pattern as pluma suggests in his answer but using the OP's naming.

查看更多
做自己的国王
7楼-- · 2019-01-03 08:38

The best solution is to refactor to your promise chain to use ES6 await's. Then you can just return from the function to skip the rest of the behavior.

I have been hitting my head against this pattern for over a year and using await's is heaven.

查看更多
登录 后发表回答