Search code examples
javascriptnode.jspromisebluebirdpg-promise

How to turn nested callback into promise?


Recently I started using pg-promise with bluebird library. I have always been nesting callback and handling err in each callback. I find that the catch statement in promise looks really neat. I am not sure if it possible to turn this code to promise base?

username = username.toUpperCase();
let text = "SELECT * FROM users WHERE username = $1";
let values = [username];
database.one(text, values).then(function (userObject) {
  // ANY WAY TO TURN this nested bycrypt into promise chain?
  bcrypt.compare(password, userObject.password, function (err, same) {
    if (err) {
      return next(err, null);
    }
    if (!same) {
      return next(new Error("Password mismatched!"), null);
    }
    const serializeObject = {_id: userObject._id};
    return next(null, serializeObject);
  });
}).catch(function (err) {
  return next(err, null);
});

Solution

  • I imagine using bluebirds promisify, you would promisify bcrypt.compare like so (you don't HAVE to use the Async part of the name)

    let compareAsync = Promise.promisify(bcrypt.compare);
    

    because userObject in the first .then needs to be used in the second .then, you can't simply chain the .then's by returning compareAsync, because then the next .then wont have access to userObject

    one fix, is to use a variable that will be in scope for both .then's (but ugh)

    username = username.toUpperCase();
    let text = "SELECT * FROM users WHERE username = $1";
    let values = [username];
    let uo; //hacky the outer scoped variable
    database.one(text, values).then(function (userObject) {
        uo = userObject;
        return compareAsync(password, userObject.password);
    }).then(function(same) {
        if (!same) {
            throw new Error("Password mismatched!");
        }
        const serializeObject = {_id: uo._id};
        return next(null, serializeObject);
    }).catch(function (err) {
        return next(err, null);
    });
    

    another (in my opinion cleaner) option is a nested .then

    username = username.toUpperCase();
    let text = "SELECT * FROM users WHERE username = $1";
    let values = [username];
    database.one(text, values).then(function (userObject) {
        return compareAsync(password, userObject.password)
        // [optional] following three lines to generate a "nicer" error for compare failure
        .catch(function(err) {
            throw "bcrypt.compare failed";
        })
        // nested .then to pass on the userObject and same at the same time
        .then(function (same) {
            return { same: same, userObject: userObject };
        });
    }).then(function (result) {
        let same = result.same,
            userObject = result.userObject;
    
        if (!same) {
            throw new Error("Password mismatched!");
        }
        let serializeObject = { _id: userObject._id };
        return next(null, serializeObject);
    }).catch(function (err) {
        return next(err, null);
    });
    

    NOTE: bluebird has a promisifyAll function ... that promisifies functions in an object, and adds (by default) the Async postfix to the function name - I believe you can decide on a different postfix name, but the documentation will tell you more

    when promisifying a single function, you declare the name yourself - the above could've easily been

    let trumpIsBigly = Promise.promisify(bcrypt.compare);
    

    then you would just use trumpIsBigly where the code has compareAsync

    One last possibility

    A hand rolled promisified compareAsync (lifted mostly from vitaly-t's answer but with additions)

    function compareAsync(password1, password2, inValue) {
        return new Promise(function (resolve, reject) {
            bcrypt.compare(password1, password2, function (err, same) {
                err = err || (!same && new Error("Password mismatched!"));
                if (err) {
                    reject(err);
                } else {
                    resolve(inValue);
                }
            });
    
        });
    }
    

    Now compareAsync will resolve to the incoming value inValue only if there's no error, AND same is true

    username = username.toUpperCase();
    let text = "SELECT * FROM users WHERE username = $1";
    let values = [username];
    database.one(text, values).then(function (userObject) {
        return compareAsync(password, userObject.password, userObject)
    }).then(function (userObject) {
        let serializeObject = { _id: userObject._id };
        return next(null, serializeObject);
    }).catch(function (err) {
        return next(err, null);
    });
    

    Which makes the "chain" very simple!