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
Check If Object Is a Jquery Object
Extending Built-In Natives in Es6 with Babel
Differencebetween Typeof and Instanceof and When Should One Be Used VS. the Other
Executing JavaScript from Python
How to Add/Subtract Dates with JavaScript
Leaflet Drawing Tiles Disjointly
Selecting an Element in Iframe with Jquery
Does Console.Log Invokes Tostring Method of an Object
How to Consume Http Component Efficiently in a Service in Angular 2 Beta
Regex Created via New Regexp(Mystring) Not Working (Backslashes)
Node.Js Tail-Call Optimization: Possible or Not
Fulfill (Don't Resolve) Promise with Another Promise
How to Remove a Key from a JavaScript Object
Why the Function Called by Settimeout Has No Callstack Limit
How to Pass Parameters to a Script Tag