I am still fairly new to promises and am using bluebird currently, however I have a scenario where I am not quite sure how to best deal with it.
So for example I have a promise chain within an express app like so:
repository.Query(getAccountByIdQuery)
.catch(function(error){
res.status(404).send({ error: "No account found with this Id" });
})
.then(convertDocumentToModel)
.then(verifyOldPassword)
.catch(function(error) {
res.status(406).send({ OldPassword: error });
})
.then(changePassword)
.then(function(){
res.status(200).send();
})
.catch(function(error){
console.log(error);
res.status(500).send({ error: "Unable to change password" });
});
So the behaviour I am after is:
- Goes to get account by Id
- If there is a rejection at this point, bomb out and return an error
- If there is no error convert the document returned to a model
- Verify the password with the database document
- If the passwords dont match then bomb out and return a different error
- If there is no error change the passwords
- Then return success
- If anything else went wrong, return a 500
So currently catches do not seem to stop the chaining, and that makes sense, so I am wondering if there is a way for me to somehow force the chain to stop at a certain point based upon the errors, or if there is a better way to structure this to get some form of branching behaviour, as there is a case of if X do Y else Z
.
Any help would be great.
.catch
works like thetry-catch
statement, which means you only need one catch at the end:No. You cannot really "end" a chain, unless you throw an exception that bubbles until its end. See Benjamin Gruenbaum's answer for how to do that.
A derivation of his pattern would be not to distinguish error types, but use errors that have
statusCode
andbody
fields which can be sent from a single, generic.catch
handler. Depending on your application structure, his solution might be cleaner though.Yes, you can do branching with promises. However, this means to leave the chain and "go back" to nesting - just like you'd do in an nested if-else or try-catch statement:
I think Benjamin Gruenbaum's answer above is the best solution for a complex logic sequence, but here is my alternative for simpler situations. I just use an
errorEncountered
flag along withreturn Promise.reject()
to skip any subsequentthen
orcatch
statements. So it would look like this:If you have more than two then/catch pairs, you should probably use Benjamin Gruenbaum's solution. But this works for a simple set-up.
Note that the final
catch
only hasreturn;
rather thanreturn Promise.reject();
, because there's no subsequentthen
that we need to skip, and it would count as an unhandled Promise rejection, which Node doesn't like. As is written above, the finalcatch
will return a peacefully resolved Promise.I have been doing this way:
You leave your catch in the end. And just throw an error when it happens midway your chain.
Your other functions would probably look something like this:
Instead of
.then().catch()...
you can do.then(resolveFunc, rejectFunc)
. This promise chain would be better if you handled things along the way. Here is how I would rewrite it:Note: The
if (error != null)
is a bit of a hack to interact with the most recent error.This behavior is exactly like a synchronous throw:
That's half of the point of
.catch
- to be able to recover from errors. It might be desirable to rethrow to signal the state is still an error:However, this alone won't work in your case since the error be caught by a later handler. The real issue here is that generalized "HANDLE ANYTHING" error handlers are a bad practice in general and are extremely frowned upon in other programming languages and ecosystems. For this reason Bluebird offers typed and predicate catches.
The added advantage is that your business logic does not (and shouldn't) have to be aware of the request/response cycle at all. It is not the query's responsibility to decide which HTTP status and error the client gets and later as your app grows you might want to separate the business logic (how to query your DB and how to process your data) from what you send to the client (what http status code, what text and what response).
Here is how I'd write your code.
First, I'd get
.Query
to throw aNoSuchAccountError
, I'd subclass it fromPromise.OperationalError
which Bluebird already provides. If you're unsure how to subclass an error let me know.I'd additionally subclass it for
AuthenticationError
and then do something like:As you can see - it's very clean and you can read the text like an instruction manual of what happens in the process. It is also separated from the request/response.
Now, I'd call it from the route handler as such:
This way, the logic is all in one place and the decision of how to handle errors to the client is all in one place and they don't clutter eachother.