Using the https://github.com/kriskowal/q library, I'm wondering if it's possible to do something like this:
// Module A
function moduleA_exportedFunction() {
return promiseReturningService().then(function(serviceResults) {
if (serviceResults.areGood) {
// We can continue with the rest of the promise chain
}
else {
performVerySpecificErrorHandling();
// We want to skip the rest of the promise chain
}
});
}
// Module B
moduleA_exportedFunction()
.then(moduleB_function)
.then(moduleB_anotherFunction)
.fail(function(reason) {
// Handle the reason in a general way which is ok for module B functions
})
.done()
;
Basically if the service results are bad, I'd like to handle the failure in module A, using logic that is specific to the internals of module A, but still skip the remaining module B functions in the promise chain.
The obvious solution for skipping module B functions is to throw an error/reason from module A. However, then I would need to handle that in module B. And ideally I'd like to do it without needing any extra code in module B for that.
Which may very well be impossible :) Or against some design principles of Q.
In which case, what kind of alternatives would you suggest?
I have two approaches in mind, but both have their disadvantages:
Throw a specific error from module A and add specific handling code to module B:
.fail(function(reason) { if (reason is specificError) { performVerySpecificErrorHandling(); } else { // Handle the reason in a general way which is ok for module B functions } })
Perform the custom error handling in module A, then after handling the error, throw a fake rejection reason. In module B, add a condition to ignore the fake reason:
.fail(function(reason) { if (reason is fakeReason) { // Skip handling } else { // Handle the reason in a general way which is ok for module B functions } })
Solution 1 requires adding module A specific code to module B.
Solution 2 solves this, but the whole fake rejection approach seems very hackish.
Can you recommend other solutions?
It is kind of a design thing. In general, when a module or service returns a promise, you want it to resolve if the call was successful, and to fail otherwise. Having the promise neither resolve or fail, even though you know the call was unsuccessful, is basically a silent failure.
But hey, I don't know the specifics of your modules, or reasons, so if you do want to fail silently in this case, you can do it by returning an unresolved promise:
// Module A
Let's talk about control constructs.
In JavaScript, code flows in two ways when you call a function.
return
a value to the caller, indicating that it completed successfully.throw
an error to the caller, indicating that an exceptional operation occurred.It looks something like:
Promises model this exact same behavior.
In Promises, code flows in exactly two ways when you call a function in a
.then
handler:return
a promise or a value which indicates that it completed successfully.throw
an error which indicates that an exceptional state occured.It looks something like:
Promises model flow of control itself
A promise is an abstraction over the notion sequencing operations itself. It describes how control passes from one statement from another. You can consider
.then
an abstraction over a semicolon.Let's talk about synchronous code
Let's see how synchronous code would look in your case.
So, how continuing with the rest of our code is simply
returning
. This is the same in synchronous code and in asynchronous code with promises. Performing very specific error handling is also ok.How would we skip the rest of the code in the synchronous version?
Well, even if not immediately, there is a rather simple way to make doD never execute by causing doC to enter into an infinite loop:
So, it is possible to never resolve a promise - like the other answer suggests - return a pending promise. However, that is extremely poor flow control since the intent is poorly conveyed to the consumer and it will be likely very hard to debug. Imagine the following API:
A bit confusing, isn't it :)? However, it actually exists in some places. It's not uncommon to find the following in really old APIs.
So, what bothers us about terminating the process in that API anyway?
It's all about responsibility.
In your case. ModelA is simply breaching the limit of its responsibility, it should not be entitled to make such decisions about the flow of the program. Whoever consumes it should make these decisions.
Throw
The better solution is to throw an error and let the consumer handle it. I'll use Bluebird promises here since they're not only two orders of magnitude faster and have a much more modern API - they also have much much better debugging facilities - in this case - sugar for conditional catches and better stack traces:
So in a line - you would do what you would do in synchronous code, as is usually the case with promises.
Note Promise.method is just a convenience function Bluebird has for wrapping functions, I just hate synchronous throwing in promise returning APIs as it creates major breakage.
Inspired by Benjamin Gruenbaum's comments and answer - if I was writing this in synchronous code, I would make
moduleA_exportedFunction
return ashouldContinue
boolean.So with promises, it would basically be something like this (disclaimer: this is psuedo-code-ish and untested)
It does require some handling code in module B, but the logic is neither specific to module A's internals nor does it involve throwing and ignoring fake errors - mission accomplished! :)