Break Promise Chain and Call a Function Based on the Step in the Chain Where It Is Broken (Rejected)

Break promise chain and call a function based on the step in the chain where it is broken (rejected)

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.

Break javascript promise chain in a clean way

In my understanding, step 2 should break the chain...

It would, but you've accidentally converted that rejection into a resolution.

The key thing about promises is that every call to then creates a new promise which is resolved/rejected based on what the then callback(s) do, and the callback processing a rejection converts that rejection into a resolution unless it intentionally does otherwise.

So here:

return step(2)
.then(null, function() { // This handler converts the
stepError(2); // rejection into a resolution
}); // with the value `undefined`

That's so that you can have error handlers that compensate for the error.

Since stepError returns a rejection, you could continue the rejection by just adding a return:

return step(2)
.then(null, function() {
return stepError(2); // Added `return`
});

...or alternately, remove that handler entirely:

return step(2);

...or you could throw in the callback, which is automatically turned into a rejection.

The unhandled rejection warning is caused by the fact nothing consumes the rejection from stepError.


Here's an example returning the result of stepError:

Promise.resolve()    .then(function() {        return step(1)            .then(null, function() {                return stepError(1); // Added `return`            });    })    .then(function() {        return step(2)            .then(null, function() {                return stepError(2); // Added `return`            });    })    .then(function() {        return step(3)            .then(null, function() {                return stepError(3); // Added `return`            });    });
function step(n) { console.log('Step '+n); return (n === 2) ? Promise.reject(n) : Promise.resolve(n);}
function stepError(n) { console.log('Error '+n); return Promise.reject(n);}

how to break promise chain

I think you don't want a chain here. In a synchronous fashion, you'd have written

function getMode(){
if (checkIf('A')) {
return 'A';
} else {
if (checkIf('B')) {
return 'B';
} else {
if (checkIf('C')) {
return 'C';
} else {
throw new Error();
}
}
}
}

and this is how it should be translated to promises:

function getMode(){
checkIf('A').then(function(bool) {
if (bool)
return 'A';
return checkIf('B').then(function(bool) {
if (bool)
return 'B';
return checkIf('C').then(function(bool) {
if (bool)
return 'C';
throw new Error();
});
});
});
}

There is no if else-flattening in promises.

Break a .finally() chain

This is achievable with then and catch. You should not use finally if you don't want the callback to run in case of an error.

I have to sequentially call different endpoints in order even if the previous one fails. However in case an endpoint timeouts I want to break the chain

So you want to not call them when the previous one fails (with a timeout), all you want to do is to ignore non-timeout errors. That's what catch should be used for:

function callEndpoint(action) {
return axios.get(actionUrl, { params: { action } }).catch(err => {
if (isTimeout(err))
throw err
else
; // ignore the error, return undefined
})
}

Then just chain them:

callEndpoint('action3').then(() => callEndpoint('action6')).then(() => callEndpoint('action3'))

Breaking a promise chain (with multiple happy flows and single chain)

Other than using promise rejection, a possible approach to clean the code up could be to organize the happy paths into their own chains which get combined into a single chain.

In other words, every sequence of steps that should happen, well, sequentially, should be organized together.

In the particular snippet you provided, you always go right after you went left, so just make that an explicit chain that happens if the number of steps taken is even:

const happyPath = total_steps_taken => turnLeft(total_steps_taken)
.then(turnRight)
.then(logSteps);

goStraight(0).then(total_steps_taken => {
if (total_steps_taken % 2 === 0) {
return happyPath(total_steps_taken);
}
});

Full example:

const stepPromise = direction => x => {  console.log(`going ${x} steps ${direction}`);  return new Promise(resolve => {    setTimeout(() => resolve(x), 500);  });}
const turnLeft = stepPromise('left')const turnRight = stepPromise('right');const goStraight = stepPromise('straight');const logSteps = steps => console.log(`steps taken: ${steps}`);
const happyPath = steps => turnLeft(steps) .then(turnRight) .then(steps => console.log(`Took ${steps} steps`));
const branchOrQuit = steps => { if (steps % 2 === 0) { console.log('going down the happy path'); return happyPath(steps); } console.log('quitting');}
const happyExample = () => { console.log('\n --- EVEN ---'); return goStraight(2).then(branchOrQuit);}

const sadExample = () => { console.log('\n --- ODD ---'); return goStraight(1).then(branchOrQuit);}
happyExample().then(sadExample);

Promise: skip all fulfill and reject reactions but execute .finally

Is it possible to skip all subsequent .then(...) and .catch(...) calls within one of such handlers to go directly to .finally()?

No.

Currenly I stop the chain by just infinitely waiting - yet this approach of pending Promise is an obstacle.

Indeed, don't do that. You can skip then handlers by using rejections (exceptions) for flow control, but the more appropriate way is to handle this by nesting the part of the chain to be skipped inside an if statement.

This is done to handle certain API responses that have common response handling (like responses with code >= 500)

For that, you should use something like

return axios.request(endpoint).then(response => {

}).catch(error => {
if (api.handleCommonError(error)) return; // returns false if it couldn't handle the error

}).finally(() => {

});

Yes, you cannot hide this kind of error handling inside an api.request function.



Related Topics



Leave a reply



Submit