LukeP LukeP - 4 months ago 11
Javascript Question

Promise chain not executing in expected order

I'm trying to use a sequence of promises to authenticate a user name and password, but the promises seen to be resolving too early.

I have two methods from two different files involved here.

First, my controller, which picks up the request and invokes my model for the authentication. The model is supposed to return a promise that will have authentication results when it resolves.

Here is my controller:

router.post('/api/auth-tokens', function (req, res, next) {
var creds = {user: req.body.user, password: req.body.password};
people.authenticate(creds)
.then(function (result) {
var status = result.valid ? 200: 401;
res.sendStatus(status);
});
});


people
is the lexically scoped model. Here is the current implementation of the method used above:

var bluebird = require('bluebird');
var bcrypt = require('bcrypt');
var compare = bluebird.promisify(bcrypt.compare);

exports.authenticate = function (args) {
var promise = orm.select({
db: db,
table: table,
where: {id: args.user},
qrm: 'one'
});

promise.catch(function (err) {
var auth_info = { valid: false };
return auth_info;
});

promise.then(function (user) {
var promise = compare(args.password, user.hash)
.then(function (same) {
var auth_info = { valid: same, permissions: user.permissions };
return auth_info;
});
return promise;
});

return promise;
};


orm
returns a promise that will throw an error if the user doesn't exist, or resolve and yield the database row if the user was found. (That is why I have that
promise.catch
invocation in there. It's in case the user doesn't exist)

If the user does indeed exist, I want to invoke
bcrypt.compare
to compare the input to the hash contained in the database row. When that finishes asynchronously, I want to resolve the promise chain and yield control back to the controller, returning the
results
object.

The issue I'm having though is that the
.then
in my controller is being executed immediately with the results of the initial promise returned by the call to
orm.select
. Can anyone explain why this is happening and how I can fix it?

Answer

It's happening because you've hooked up that callback directly to the promise returned by orm.select. Instead, you need to use the promise returned by then.

This is one of the biggest key things about promises: then returns a new promise. (So does catch.) If your handler function returns a promise (technically, any thenable) from either then or catch, the new promise that then (or catch) returns will resolve/reject based on the promise you return. If your handler function returns a non-promise value, the new promise from then (or catch) is resolved (not rejected) with that value.

So authenticate should probably look like this:

exports.authenticate = function (args) {
    return orm.select({
        db: db,
        table: table,
        where: {id: args.user},
        qrm: 'one'
    })
    .then(function (user) {
        return compare(args.password, user.hash)
        .then(function (same) {
            var auth_info = { valid: same, permissions: user.permissions };
            return auth_info;
        });
    })
    .catch(function (err) {
        var auth_info = { valid: false };
        return auth_info;
    });
};

Note that the changes are both to your outer function and what you do in then. Also note that the code in your catch callback will be called whether it's the original promise from orm.select that was rejected, or the one from compare.

To see what's going on, compare this snippet with the one after:

// Returning the original promise
// Usually NOT what you want
function foo() {
  var promise = new Promise(function(resolve) {
    console.log("Starting first promise");
    setTimeout(function() {
      console.log("Resolving first promise");
      resolve("first");
    }, 500);
  });
  promise.then(function() {
    promise = new Promise(function(resolve) {
      console.log("Starting second promise");
      setTimeout(function() {
        console.log("Resolving second promise");
        resolve("second");
      }, 500);
    });
  });
  return promise;
}
foo().then(function(result) {
  console.log("Got result: " + result);
});

Second:

// Returning the original promise
// Usually NOT what you want
function foo() {
  return new Promise(function(resolve) {
    console.log("Starting first promise");
    setTimeout(function() {
      console.log("Resolving first promise");
      resolve("first");
    }, 500);
  })
  .then(function() {
    return new Promise(function(resolve) {
      console.log("Starting second promise");
      setTimeout(function() {
        console.log("Resolving second promise");
        resolve("second");
      }, 500);
    });
  });
}
foo().then(function(result) {
  console.log("Got result: " + result);
});