Let's say I have this function authenticate that returns a promise. The promise then resolves with the result. False and true are expected outcomes, as I see it, and rejections should only occur in an error case. Or, is a failure in authentication considered something you would reject a promise for?
Good question! There is no hard answer. It depends on what you consider to be exceptional at that specific point of the flow.
Rejecting a Promise
is the same a raising an exception. Not all undesired results are exceptional, the result of errors. You could argue your case both ways:
Failed authentication should
reject
thePromise
, because the caller is expecting aUser
object in return, and anything else is an exception to this flow.Failed authentication should
resolve
thePromise
, albeit tonull
, since providing the wrong credentials is not really an exceptional case, and the caller should not expect the flow to always result in aUser
.
Note that I'm looking at the issue from the caller's side. In the flow of information, does the caller expect his actions to result in a User
(and anything else is an error), or does it make sense for this particular caller to handle other results?
In a multi-layered system, the answer may change as the data flows through the layers. For example:
- HTTP layer says RESOLVE! The request was sent, the socket closed cleanly, and the server emitted a valid response. The Fetch API does this.
- Protocol layer then says REJECT! The status code in the response was 401, which is ok for HTTP, but not for the protocol!
- Authentication layer says NO, RESOLVE! It catches the error, since 401 is the expected status for a wrong password, and resolves to a
null
user. - Interface controller says NONE OF THAT, REJECT! The modal showing on screen was expecting a username and an avatar, and anything other than that information is an error at this point.
This 4-point example is obviously complicated, but it illustrates 2 points:
- Whether something is an exception/rejection or not depends on the surrounding flow and the expectations
- Different layers of your program may treat the same result differently, as they are located in varying stages of the flow
So again, no hard answer. Time to think and design!
-
1
So Promises have a nice property that they bring JS from functional languages, which is that they indeed implement this Either
type-constructor that glues together two other types, the Left
type and the Right
type, by forcing the logic to either take the one branch or the other branch.
data Either x y = Left x | Right y
Now you're noticing indeed that the type on the left-hand-side is ambiguous for promises; you can reject with anything. This is true because JS is weakly typed, but you want to be cautious if you're programming defensively.
The reason is that JS will take throw
statements from promise-handling code and bundle it up into the Left
side of that too. Technically in JS you can throw
everything, including true/false or a string or a number: but JavaScript code also throws things without throw
(when you do things like trying to access properties on nulls) and there is a settled API for this (the Error
object). So when you get around to catching, it's usually nice to be able to assume that those errors are Error
objects. And since the reject
for the promise will agglomerate in any errors from any of the above bugs, you generally want to only throw
other errors, to make your catch
statement have a simple, consistent logic.
Therefore although you can put an if-conditional in your catch
and look for false errors, in which case the truth case is trivial,
Either (Either Error ()) ()
you will probably prefer the logic structure, at least for what comes immediately out of the authenticator, of a simpler boolean:
Either Error Bool
In fact the next level of authentication logic is probably to return some sort of User
object containing the authenticated user, so that this becomes:
Either Error (Maybe User)
and this is more or less what I'd expect: return null
in the case where the user is not defined, otherwise return {user_id: <number>, permission_to_launch_missiles: <boolean>}
. I'd expect that the general case of not being logged in is salvageable, for example if we're in some sort of "demo to new clients" mode, and should not be mixed in with bugs where I accidentally called object.doStuff()
when object.doStuff
was undefined
.
Now with that said, what you may want to do is to define a NotLoggedIn
or PermissionError
exception which derives from Error
. Then in the things which really need it you want to write:
function launchMissiles() {
function actuallyLaunchThem() {
// stub
}
return getAuth().then(auth => {
if (auth === null) {
throw new PermissionError('Cannot launch missiles without permission, cannot have permission if not logged in.');
} else if (auth.permission_to_launch_missiles) {
return actuallyLaunchThem();
} else {
throw new PermissionError(`User ${auth.user_id} does not have permission to launch the missiles.`);
}
});
}
Errors
Let's talk about errors.
There are two types of errors:
- expected errors
- unexpected errors
- off-by-one errors
Expected Errors
Expected errors are states where the wrong thing happens but you know that it might, so you deal with it.
These are things like user input or server requests. You know the user might make a mistake or that the server might be down, so you write some checking code to make sure that the program asks for input again, or displays a message, or whatever other behavior is appropriate.
These are recoverable when handled. If left uhandled, they become unexpected errors.
Unexpected Errors
Unexpected errors (bugs) are states where the wrong thing happens because the code is wrong. You know that they'll eventually happen, but there's no way to know where or how to deal with them because, by definition, they're unexpected.
These are things like syntax and logic errors. You may have a typo in your code, you may have called a function with the wrong parameters. These aren't typically recoverable.
try..catch
Let's talk about try..catch
.
In JavaScript, throw
isn't commonly used. If you look around for examples in code they're going to be few and far between, and usually structured along the lines of
function example(param) {
if (!Array.isArray(param) {
throw new TypeError('"param" should be an array!');
}
...
}
Because of this, try..catch
blocks aren't all that common for control flow either. It's usually pretty easy to add some checks before calling methods to avoid expected errors.
JavaScript environments are pretty forgiving as well, so unexpected errors are often left uncaught as well.
try..catch
doesn't have to be uncommon. There are some nice use cases, which are more common in languages such as Java and C#. Java and C# have the advantage of typed catch
constructs, so that you can differentiate between expected and unexpected errors:
try
{
var example = DoSomething();
}
catch (ExpectedException e)
{
DoSomethingElse(e);
}
This example allows other unexpected exceptions to flow up and be handled elsewhere (such as by being logged and closing the program).
In JavaScript, this construct can be replicated via:
try {
let example = doSomething();
} catch (e) {
if (e instanceOf ExpectedError) {
DoSomethingElse(e);
} else {
throw e;
}
}
Not as elegant, which is part of the reason why it's uncommon.
Functions
Let's talk about functions.
If you use the single responsibility principle, each class and function should serve a singular purpose.
For example authenticate()
might authenticate a user.
This might be written as:
const user = authenticate();
if (user == null) {
// keep doing stuff
} else {
// handle expected error
}
Alternatively it might be written as:
try {
const user = authenticate();
// keep doing stuff
} catch (e) {
if (e instanceOf AuthenticationError) {
// handle expected error
} else {
throw e;
}
}
Both are acceptible.
Promises
Let's talk about promises.
Promises are an asynchronous form of try..catch
. Calling new Promise
or Promise.resolve
starts your try
code. Calling throw
or Promise.reject
sends you to the catch
code.
Promise.resolve(value) // try
.then(doSomething) // try
.then(doSomethingElse) // try
.catch(handleError) // catch
If you have an asynchronous function to authenticate a user, you might write it as:
authenticate()
.then((user) => {
if (user == null) {
// keep doing stuff
} else {
// handle expected error
}
});
Alternatively it might be written as:
authenticate()
.then((user) => {
// keep doing stuff
})
.catch((e) => {
if (e instanceOf AuthenticationError) {
// handle expected error
} else {
throw e;
}
});
Both are acceptible.
Nesting
Let's talk about nesting.
try..catch
can be nested. Your authenticate()
method might internally have a try..catch
block such as:
try {
const credentials = requestCredentialsFromUser();
const user = getUserFromServer(credentials);
} catch (e) {
if (e instanceOf CredentialsError) {
// handle failure to request credentials
} else if (e instanceOf ServerError) {
// handle failure to get data from server
} else {
throw e; // no idea what happened
}
}
Likewise promises can be nested. Your async authenticate()
method might internally use promises:
requestCredentialsFromUser()
.then(getUserFromServer)
.catch((e) => {
if (e instanceOf CredentialsError) {
// handle failure to request credentials
} else if (e instanceOf ServerError) {
// handle failure to get data from server
} else {
throw e; // no idea what happened
}
});
So what's the answer?
Ok, I think it's time for me to actually answer the question:
Is a failure in authentication considered something you would reject a promise for?
The simplest answer I can give is that you should reject a promise anywhere you would otherwise want to throw
an exception if it were synchronous code.
If your control flow is simpler by having a few if
checks in your then
statements, there's no need to reject a promise.
If your control flow is simpler by rejecting a promise and then checking for types of errors in your error handling code, then do that instead.
I've used the "reject" branch of a Promise to represent the "cancel" action of jQuery UI dialog boxes. It seemed more natural than using the "resolve" branch, not least because there's often multiple "close" options on a dialog box.
-
Most purists I know would disagree with you. – user42386 Dec 2 '16 at 15:47
Handling a promise is more or less like "if" condition. It's up to you whether you want to "resolve" or "reject" if authentication failed.
-
1
-
@zzzBox so by that logic, you should use a Promise as a asynchronous
try...catch
and simply say that if you were able to complete and get a result, you should resolve regardless of the value that was received, otherwise you should reject? – somethinghere Dec 2 '16 at 15:18 -
@somethinghere, no, you've misconstrued my argument.
try { if (!doSomething()) throw whatever; doSomethingElse() } catch { ... }
is perfectly fine, but the construct that aPromise
represents is thetry..catch
part, not theif
part. – zzzzBov Dec 2 '16 at 15:22 -
@zzzzBov I got that in all fairness :) I like the analogy. But my logic is simply that if
doSomething()
fails, then it will throw, but if not it could contain the value you need (yourif
above is slightly confusing as its not part of your idea here :)). You should only reject if there is a reason to throw (in the analogy), so if the test failed. If the test succeeded you should always resolve, regardless of whether its value is positive, right? – somethinghere Dec 2 '16 at 15:25 -
@somethinghere, I've decided to write up an answer (assuming this stays open long enough), because comments are not enough for expressing my thoughts. – zzzzBov Dec 2 '16 at 15:27
reject
and not return false, but if you are expecting the value to be aBool
, then you were successful and you should resolve with the Bool regardless of the value. Promises are sort of proxies for values - they store the returned value, so only if the value could not be obtained should youreject
. Otherwise you shouldresolve
. – somethinghere Dec 2 '16 at 15:15false
or as throwing an exception? – wrschneider Dec 2 '16 at 15:19then
when the server responds - even if an error code is returned - and you must check theresponse.ok
. Thecatch
handler is only triggered for unexpected errors. – CodingIntrigue Dec 2 '16 at 15:21