22

I am thinking about when exactly I need to reject a promise. I found a couple of questions regarding this topic, but could not find a proper answer. When should I reject a promise?

This article http://howtonode.org/6666a4b74d7434144cff717c828be2c3953d46e7/promises says:

  • Resolve: A successful Promise is 'resolved' which invokes the success listeners that are waiting and remembers the value that was resolved for future success listeners that are attached. Resolution correlates to a returned value.
  • Reject: When an error condition is encountered, a Promise is 'rejected' which invokes the error listeners that are waiting and remembers the value that was rejected for future error listeners that are attached. Rejection correlates to a thrown exception.

Is this the principle guideline? That one only reject a promise if an exception occured?

But in case of a function like

findUserByEmail()

I'd would expect the function to return a user, so that I can continue the chain without verifying the result

findUserByEmail()
    .then(sendWelcomeBackEmail)
    .then(doSomeNiceStuff)
    .then(etc..)

What are best / common practises?

Community
  • 1
  • 1
Christopher Will
  • 2,991
  • 3
  • 29
  • 46
  • Your description of rejection may be incorrect as I understand it, if by "future" you mean "down the chain". When an error handler runs, the return value is used to **fulfill** (not **reject**) subsequent promises. So once an error handler runs, the error is considered handled, and execution proceeds on the success side. To put it a different way, if you want to handle an error in successive steps in the chain, the first error handler will need to rethrow it in order for subsequent error handlers in the chain to be invoked, just like try/catch. –  Jul 02 '13 at 06:38
  • FYI that article you quote is a bit inaccurate; it says "resolve" when it should say "fulfill." – Domenic Jul 05 '13 at 20:29

2 Answers2

24

In general you can think of rejecting as being analogous to a synchronous throw and fulfilling as being analogous to a synchronous return. You should reject whenever the function is unsuccessful in some way. That could be a timeout, a network error, incorrect input etc. etc.

Rejecting a promise, just like throwing an exception, is useful for control flow. It doesn't have to represent a truly unexpected error; it can represent a problem that you fully anticipate and handle:

function getProfile(email) {
  return getProfileOverNetwork(email)
    .then(null, function (err) {
      //something went wrong getting the profile
      if (err.code === 'NonExistantUser') {
        return defaultUser;
      } else if (profileCached(email)) {
        return getProfileFromCache(email);//fall back to cached profile
      } else {
        throw err;//sometimes we don't have a nice way of handling it
      }
    })
}

The rejection lets us jump over the normal success behavior until we get to a method that knows how to handle it. As another example, we might have some function that's deeply nested at the bottom of the applications stack, which rejects. This might not be handled until the very top of the stack, where we could log it. The point is that rejections travel up the stack just like exceptions do in synchronous code.

In general, wherever possible, if you are struggling to write some asynchronous code, you should think "what would I write if this were synchronous". It's usually a fairly simple transformation to get from that, to the promised equivalent.

A nice example of where rejected promises might be used is in an exists method:

function exists(filePath) {
  return stat(filePath) //where stat gets last updated time etc. of the file
    .then(function () { return true; }, function () { return false; })
}

Notice how in this case, the rejection is totally expected and just means the file does not exist. Notice also how it parallels the synchronous function though:

function existsSync(filePath) {
  try {
    statSync(filePath);
    return true;
  } catch (ex) {
    return false;
  }
}

Your Example

Returning to your example:

I would normally chose to reject the promise resulting from findUserByEmail if no user was found. It's something you fully expect to happen sometimes, but it's the exception from the norm, and should probably be handled pretty similarly to all other errors. Similarly if I were writing a synchronous function I would have it throw an exception.

Sometimes it might be useful to just return null instead, but this would depend on your applications logic and is probably not the best way to go.

Ayman Safadi
  • 11,502
  • 1
  • 27
  • 41
ForbesLindesay
  • 10,482
  • 3
  • 47
  • 74
  • Thanks, as expected it's more some kind of philosophical question. Because your comparison with a synchronous exception to be thrown on no user found could be argued from both sides as well. But, in my oppinion, I agree with your conclusion. – Christopher Will Jun 25 '13 at 13:26
  • Yes, the point I wanted to get across is that it's the same argument, whether it's rejecting promises or throwing exceptions. – ForbesLindesay Jun 26 '13 at 01:42
  • 6
    This example is technically correct, but I would suggest that you don't use exceptions for flow control. At best, it's confusing to other programmers. At worst, it makes error reporting difficult and can cause performance to suffer. Do you really want a stack trace generated every time a user can not be found? More info: [c2.com](http://c2.com/cgi/wiki?DontUseExceptionsForFlowControl) – Ben Jun 11 '14 at 20:20
  • @ben it depends on lots of things. The performance cost may be non-trivial, but as long as not finding a user isn't tooooo frequent an occurrence, that's fine. Consider that this is typical behavior throughout lots of JavaScript libraries. For example the `fs.exists` function in node.js is implemented by calling `fs.stat` and then handling the error. – ForbesLindesay Jun 12 '14 at 10:18
  • The only thing it depends on is external libraries that throw exceptions. If you have a choice, exceptions as flow control will always be slower and add nothing compared to returning a value. Being in common use does not make it a good pattern. Even so, I do see much evidence that it is in common use. – Ben Jun 16 '14 at 18:10
  • fs.exists is an anti-pattern and should not be used (for unrelated reasons). Skimming the fs library source, it appears that the try-catch statements are catching exceptions from the native C++ or OS calls and closing file handles. This not analogous to a pure JS library that returns a Promise. – Ben Jun 16 '14 at 18:11
5

I know where you're coming from. Q and Q documentation can quite easily have you believe that deferred/promise rejection is all about exception handling.

This is not necessarily the case.

A deferred can be rejected for whatever reason your application requires.

Deferreds/promises are all about handling responses from asynchronous processes, and every asynchronous processes can result in a variety of outcomes - some of which are "successful" and some "unsuccessful". You may choose to reject your deferred - for whatever reason, regardless of whether the outcome was nominally successful or unsuccessful, and without an exception ever having been thrown, either in javascript or in the asynchronous process.

You may also choose to implement a timeout on an asynchronous process, in which case you might choose to reject a deferred without a response (successful or unsuccessful) having been received. In fact, for timeouts, this is what you would typically choose to do.

Beetroot-Beetroot
  • 18,022
  • 3
  • 37
  • 44