admin管理员组文章数量:1129645
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);
}
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);
}
Share
Improve this question
edited Dec 21, 2013 at 6:10
m59
asked Dec 21, 2013 at 1:30
m59m59
43.7k14 gold badges121 silver badges139 bronze badges
2
|
13 Answers
Reset to default 224The 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
.
Bit late to the party but this simple solution worked for me:
function chainError(err) {
return Promise.reject(err)
};
stepOne()
.then(stepTwo, chainError)
.then(stepThreee, chainError);
This allows you to break out of the chain.
What you need is a repeating .then()
chain with a special case to start and a special case to finish.
The knack is to get the step number of the failure case to ripple through to a final error handler.
- Start: call
step(1)
unconditionally. - Repeating pattern: chain a
.then()
with the following callbacks:- success: call step(n+1)
- failure: throw the value with which the previous deferered was rejected or rethrow the error.
- Finish: chain a
.then()
with no success handler and a final error handler.
You can write the whole thing out longhand but it's easier to demonstrate the pattern with named, generalised functions :
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);});
see demo
Note how in step()
, the deferred is rejected or resolved with n
, thus making that value available to the callbacks in the next .then()
in the chain. Once stepError
is called, the error is repeatedly rethrown until it is handled by finalError
.
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);
});
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(...)
.
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
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.
Try ro use this like 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);
});
If you want to solve this issue using 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 []
}
})()
If at any point you return Promise.reject('something')
you will be thrown in the catch block to the promise.
promiseOne
.then((result) => {
if (!result) {
return Promise.reject('No result');
}
return;
})
.catch((err) => {
console.log(err);
});
If the first promise does not return any result you will only get 'No result' in the console.
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.
Found Promise.prototype.catch()
examples on MDN below very helpful.
(The accepted answer mentions then(null, onErrorHandler)
which is basically the same as catch(onErrorHandler)
.)
Using and chaining the catch method
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'); });
Gotchas when throwing errors
// 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 });
If it is resolved
//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); });
Use a SequentialPromise Module
Intention
Provide a module whose responsibility is to execute requests sequentially, while tracking the current index of each operation in an ordinal manner. Define the operation in a Command Pattern for flexibility.
Participants
- Context: The object whose member method performs an operation.
- SequentialPromise: Defines an
execute
method to chain & track each operation. SequentialPromise returns a Promise-Chain from all operations performed. - Invoker: Creates a SequentialPromise instance, providing it context & action, and calls its
execute
method while passing in an ordinal list of options for each operation.
Consequences
Use SequentialPromise when ordinal behavior of Promise resolution is needed. SequentialPromise will track the index for which a Promise was rejected.
Implementation
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);
Gist
SequentialPromise
本文标签:
版权声明:本文标题:javascript - Break promise chain and call a function based on the step in the chain where it is broken (rejected) - Stack Overfl 内容由网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:http://www.betaflare.com/web/1736749717a1950973.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
Promise.prototype.catch()
examples on MDN show solution for the exact same issues. – toraritte Commented Sep 11, 2018 at 16:53