Bosiwow
Bosiwow

Reputation: 2193

Async constructor via static function, but await doesn't work

I've created a simple user class with a username, hashedpassword and passwordsalt. Because the hashing and salting of takes some time, I decided to create a static async function to create a new user (Asynchronous constructor). But somehow when I call this static async function to create a new user, the await doesn't wait for the createUser to finish. Does anybody know why?

  console.log(1);
  var newUser = await User.createUser(newUserJson);
  console.log(3);
  console.log(newUser.getPasswordHash());  //<- this line fails with the error that it cannot do the .getPasswordHash on undefined.


static async createUser(jsonUser) {
    var password = jsonUser.password;
    const saltRounds = 10;
    try {
      bcrypt.genSalt(saltRounds, function(err, salt) {
        bcrypt.hash(password, salt, function(err, hash) {
          jsonUser.passwordHash = hash;
          jsonUser.passwordSalt = salt;

          var us = new User(jsonUser);  //nonblocking. Just sets some fields.
          console.log(2);
          return us;
        });
      })
    } catch (err) {
      console.log('error')
      console.log(err)
    }
  }

My console output shows 1, 3, 2...

Does anybody know why newUser.getPasswordHash() doesn't wait for var newUser = await User.createUser(newUserJson); ?

Upvotes: 1

Views: 3858

Answers (1)

Neil Lunn
Neil Lunn

Reputation: 151132

This pretty much sums up the confusion on async/await. You seem to think you need to make something as async in order to await it. It's actually the opposite, since you mark as async in order to "contain" methods which you intend to await. So as said, you need bcrypt.genSalt and bcrypt.hash both to be wrapped in a Promise.

There's libraries to do that, or you can just roll it with the "vanilla" functions already included. And then of course you are trapping exceptions in the wrong place as well:

static createUser(jsonUser) {
    const { password } = jsonUser;
    const saltRounds = 10;

    return new Promise((resolve,reject) => 
      bcrypt.genSalt(saltRounds, (err, salt) => {
        if (err) reject(err);
        resolve(salt);
      })
    ).then(salt => new Promise((resolve,reject) => 
        bcrypt.hash(password, salt, (err, hash) => {
          if (err) reject(err);

          jsonUser.passwordHash = hash;
          jsonUser.passwordSalt = salt;

          var us = new User(jsonUser);
          resolve(us);
        })
    );
}

Or if you "really want" to use an await instead of chaining the promises with .then(), that's when you mark with async:

static async createUser(jsonUser) {                  // marked as async
    const { password } = jsonUser;
    const saltRounds = 10;

    let salt = await new Promise((resolve,reject) => // because we await here
      bcrypt.genSalt(saltRounds, (err, salt) => {
        if (err) reject(err);
        resolve(salt);
      })
    );

    return new Promise((resolve,reject) => 
        bcrypt.hash(password, salt, (err, hash) => {
          if (err) reject(err);

          jsonUser.passwordHash = hash;
          jsonUser.passwordSalt = salt;

          var us = new User(jsonUser);
          resolve(us);
        })
    );
}

So it's just defining a function that returns a Promise, and nothing more magical than that.

Then on course you actually use your await to get the value "within" a block which is marked async, and of course do the exception handling there instead. Juts using an IIFE here for demonstration:

(async function() {  // async marks the block

    try { 
      console.log(1);
      var newUser = await User.createUser(newUserJson);  // where you can await
      console.log(3);
      console.log(newUser.getPasswordHash());           // This is expected to be NOT async
    } catch(e) {
      console.error(e);
    }

})()

So there is nothing magic here at all, and the async/await is simply "sugar" for calling .then() to get the resolved value of a Promise. But the functions themselves simply are are a Promise to return, and you only mark as async where the "inner block" of code actually calls an await.

Follow that and your fine, and of course if the User.getPasswordHash() was actually an "async" function itself, then you similarly want to return a Promise from that and then await the result:

 console.log(await newUser.getPasswordHash());

But if new User() is not async, then you likely don't have async sub-methods of that.

Upvotes: 2

Related Questions