25

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?

migrated from stackoverflow.com Dec 2 '16 at 18:20

This question came from our site for professional and enthusiast programmers.

  • If authentication failed, you should reject and not return false, but if you are expecting the value to be a Bool, 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 you reject. Otherwise you should resolve. – somethinghere Dec 2 '16 at 15:15
  • This is a good question. It touches on one of the failures of the promise design. There are two types of errors, expected failures, such as when a user provides bad input (such as failing to log in), and unexpected failures, which are bugs in code. The promise design merges the two concepts into a single flow making it difficult to distinguish the two for handling. – zzzzBov Dec 2 '16 at 15:16
  • 1
    I'd say that resolving means using the response and continuing your application, while rejecting means canceling the current operation (and possibly try again or do something else). – Roberrrt Dec 2 '16 at 15:18
  • 4
    another way to think about it - if this were a synchronous method call, would you treat regular authentication failure (bad username/password) as returning false or as throwing an exception? – wrschneider Dec 2 '16 at 15:19
  • 2
    The Fetch API is a good example of this. It always triggers then when the server responds - even if an error code is returned - and you must check the response.ok. The catch handler is only triggered for unexpected errors. – CodingIntrigue Dec 2 '16 at 15:21
21

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:

  1. Failed authentication should reject the Promise, because the caller is expecting a User object in return, and anything else is an exception to this flow.

  2. Failed authentication should resolve the Promise, albeit to null, since providing the wrong credentials is not really an exceptional case, and the caller should not expect the flow to always result in a User.

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:

  1. Whether something is an exception/rejection or not depends on the surrounding flow and the expectations
  2. 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!

6

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.`);
        }
    });
}
3

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:

C#:
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.

0

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
0

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
    promise is an asynchronous try..catch, not if. – zzzzBov Dec 2 '16 at 15:17
  • @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 a Promise represents is the try..catch part, not the if 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 (your if 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

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.