Reputation: 108
I'm starting to implement promises more and more, and am uncertain as to how exactly they should be implemented.
For example, consider a signup function which takes an email address, username, and password, and performs the following async operations in sequence:
If a problem occurs during any step, progress is halted and a JSON object is sent back to the user to notify them of the problem. Otherwise the user is notified that they successfully signed up.
How should this functionality / error handling be implemented using promises?
Take the email check (which returns a promise) for example. If an error occurs querying the database, the promise should obviously be rejected. How should the actual email check be handled though?
For example, should the promise be rejected if the email address is already registered, and the error object specified in the checkEmail function?
e.g.
function checkEmail(email) {
return new Promise((resolve, reject) => {
// check email against database
// if database error occurs, reject()
if (email already registered) {
reject({error: 'email already registered'});
} else {
resolve(); // move on to check username
}
});
}
Using this style then results in promise chains that look something like this:
checkEmail(email)
.then(() => checkUsername(username))
.then(() => hashPassword(password))
.then(passwordHash => addUser(email, username, passwordHash))
.then(() => {
// notify user of successful signup
})
.catch(error => {
// all error objects are passed here
// can then simply send error object directly to client
/* if different error handling functionality is needed,
additional property can be included on error object passed to
resolve(), and used to differentiate between errors */
});
Or should the promise be resolved within the checkEmail function / the functionality of handling the checkEmail() result deferred to the next function in the chain?
e.g.
function checkEmail(email) {
return new Promise((resolve, reject) => {
// check email against database
// if database error occurs, reject()
resolve(result); // process result / potential errors in next step of chain
});
}
This then results in something similar to the following:
checkEmail(email)
.then((result) => {
if (email already registered) {
// error handling specified here, instead of in checkEmail()
return Promise.reject({error: 'email already registered'});
}
})
.then()...
.then()...
.then()...
.catch(error => {/* handle errors */});
This results in longer / more messy promise chains, but seems more flexible.
Anyway, I'm not sure exactly how to word this, so hopefully these examples illustrate what I'm trying to get at. I just started implementing promises, and am uncertain regarding much of this. Are there any problems with any of this? Is one of these methods preferable over the other? Is there a better way to implement these promise chains / handle errors?
Any help / suggestions are much appreciated.
Thanks
Upvotes: 0
Views: 66
Reputation: 707218
It's really totally up to you what you make a rejection and what you make a return value. The way promises are designed with chaining, you usually want a rejection to mean something that you want the chain to abort. If a condition is likely to want the chain to continue without special handling, then it should not reject because obviously, a rejection will abort the promise chain without special handling in a .catch()
handler to keep the chain going.
The fetch()
example is an interesting case and, I think, illustrative of a common issue where you don't just have a binary outcome of result or error. In the example of fetch()
, you have a clear error where you could not contact the server and that is indeed, a rejection. And, you have the clear result where you contacted the server successfully and got the result. Then, you have the middle case where you contacted the server successfully and got some sort of status other than 2xx (perhaps even a 404). In the case of fetch()
, they decided that a rejection was going to be defined only as they couldn't reach the server and all other return values were going to be considered a successful call and would resolve.
While I think the design decisions for fetch()
make logical sense for a generic design, there are lots and lots of cases where that isn't actually what you want and I often use a small wrapper around fetch that checks the status and resolves only when the status is 2xx. If you're trying to fetch a specific page, success in that case is only really when you got the page - everything else is just a different type of failure. So, the ideal design is really in the eyes of the caller. There often is no perfect solution.
So, here are my general guidelines:
.catch()
handler to decide whether the chain should continue or not..then()
handler.null
or something like that. A chain expecting a value should probably reject anyway. And, callers can always use .catch()
to customize if they want a more unusual behavior.fetch()
failing to even contact the server), then that's clearly a reject in all cases.In your specific case, you state this:
If a problem occurs during any step, progress is halted and a JSON object is sent back to the user to notify them of the problem. Otherwise the user is notified that they successfully signed up.
That seems pretty straightforward that any time you want progress halted, you should just reject with a descriptive error object. Then you can just run the chain and if it rejects, you have an error object to feed back to the user. If the chain didn't reject, you had success.
Take the email check (which returns a promise) for example. If an error occurs querying the database, the promise should obviously be rejected. How should the actual email check be handled though?
For example, should the promise be rejected if the email address is already registered, and the error object specified in the checkEmail function?
Per our previous discussion, if the email being already registered is something you want to abort your chain for and feed an error object back to the user, then you probably want to reject if it's already registered. If it's a benign operation that their email is already registered and the chain of operations can continue just fine, then you'd resolve when it already exists. I don't follow your logic completely, but if the user is asking to register a new account, then an email address already existing is probably an error and thus a rejection because that's an account conflict and you can't have another account with the same identifying email.
Upvotes: 1
Reputation: 664297
It depends. You can both model it with an assertNameAvailable
method that throws (or rejects a promise) when the name is already taken or with an isNameAvailable
method that returns (or fulfills a promise with) a boolean value. You would subsequently handle the result either with a try
/catch
block (or promise method) or with an if
/else
statement. Both approaches are viable.
Upvotes: 1
Reputation: 128
It depends. There are cases where passing an error by resolving the Promise is useful.
Consider the Fetch API, which resolves regardless of the response status. This is, in this context, useful is because even though the response returned with status 404, we may want to fetch a backup file later in the chain. This would not be possible had the Promise been rejected, unless we wrap it in another Promise, unless we add a catch on the Promise to handle the 404 before proceeding in down the chain.
In this specific case rejecting instead of deferring is more efficient. If the second promise in the chain depends on the success of the first one, rejecting is the most efficient way to skip the rest of the chain. Explicitly doing a check in the next promise is avoidable, because the email must be valid for us to even consider executing the rest of the chain.
It basically boils down to this: If the error is of interest to the next function in the chain resolve it, else reject it.
Upvotes: 0