1

I came across some issues with async await when instantiating a class. I have a class user, and when instantiated I wish to automatically hash the password and assign it to the password.

class User {
    public userId: string;
    public password: string = '';

    constructor(public userName: string, public plainPassword: string, public email: string) {
        this.userId     = uuidv4()
        this.userName   = userName;
        this.email      = email.toLowerCase();
        this.hashPassword();
    }

    private async hashPassword(): Promise<void> {
        try {
            const salt = await bcrypt.genSalt(10);
            this.password = await bcrypt.hash(this.plainPassword, salt);
        } 
        catch (error) {
            throw error
        }
    }

The hash password returns nothing. But is a promise function, so when I do tests, I dont get any of the User keys... email, password, userId and username are undefined...

Can you help me?

4
  • 2
    Does this answer your question? async constructor functions in TypeScript? Commented Oct 2, 2023 at 17:15
  • The other alternative is to use hashSync instead of hash. Ref: github.com/kelektiv/node.bcrypt.js#to-hash-a-password-1 Commented Oct 2, 2023 at 17:20
  • @PrerakSola A terrible, terrible alternative for a backend app. Commented Oct 2, 2023 at 17:29
  • @AKX Agreed on the point that there are better options. But overall, it depends on the use case. I wouldn't discard it completely if it serves the purpose and down sides are acceptable. Commented Oct 2, 2023 at 17:34

2 Answers 2

1

As @akx mentioned, having async constructors in JavaScript/TypeScript is impossible. However, we can circumvent this limitation by leveraging design patterns.

Let's explore two quick alternatives:

Builder pattern

import { v4 as uuidv4 } from 'uuid';
import bcrypt from 'bcryptjs';

class User {
    public userId: string;
    public password: string = '';

    constructor(public userName: string, public plainPassword: string, public email: string) {
        this.userId = uuidv4();
        this.userName = userName;
        this.email = email.toLowerCase();
        this.plainPassword = plainPassword;
    }

    public async hashPassword(): Promise<void> {
        try {
            const salt = await bcrypt.genSalt(10);
            this.password = await bcrypt.hash(this.plainPassword, salt);
        } catch (error) {
            throw error;
        }
    }
}

class UserBuilder {
    private user?: User;

    public initialize(userName: string, plainPassword: string, email: string): UserBuilder {
        this.user = new User(userName, plainPassword, email);
        return this;
    }

    public async build(): Promise<User|null> {
        if (this.user) {
            await this.user.hashPassword();
            return this.user;
        }
        return null;
    }
}

async function clientCode() {
    const user = await new UserBuilder()
        .initialize('John Doe', 'password', '[email protected]')
        .build();
        
    console.log(user);
}

clientCode();

Factory pattern

import { v4 as uuidv4 } from 'uuid';
import bcrypt from 'bcryptjs';

class User {
    public userId: string;
    public password: string = '';

    private constructor(public userName: string, public plainPassword: string, public email: string) {
        this.userId = uuidv4();
        this.userName = userName;
        this.email = email.toLowerCase();
        this.plainPassword = plainPassword;
    }

    public async hashPassword(): Promise<void> {
        try {
            const salt = await bcrypt.genSalt(10);
            this.password = await bcrypt.hash(this.plainPassword, salt);
        } catch (error) {
            throw error;
        }
    }

    // factory method to create a User instance
    public static async createUser(userName: string, plainPassword: string, email: string): Promise<User> {
        const user = new User(userName, plainPassword, email);
        await user.hashPassword();
        return user;
    }
}

// Using the factory method
async function clientCode() {
    const user = await User.createUser('John Doe', 'password', '[email protected]');
    console.log(user);
}

clientCode();

In essence, both patterns allow you to control the instantiation process, enhancing the readability and maintainability of your code. While these patterns may not offer a direct solution to the absence of async constructors in JavaScript/TypeScript, they effectively address the underlying problem.

Sign up to request clarification or add additional context in comments.

1 Comment

Thats perfect. The Factory pattern is the one most suitable for my thing... The builder pattern I seen is adding more complexity. Thank you
1

You're not waiting for the asynchronous function to finish, and there's no way to do so in a constructor (since a constructor can't be async).

If you really want hashing to occur asynchronously "in" the constructor, the password field would have to be a promise of a password that will get eventually fulfilled:

async function hashPassword(
  password: string,
): Promise<string> {
  const salt = await bcrypt.genSalt(10);
  return bcrypt.hash(password, salt);
}

class User {
  public userId: string;
  public passwordHashPromise: Promise<string>;

  constructor(
    public userName: string,
    public plainPassword: string,
    public email: string,
  ) {
    this.userId = uuidv4();
    this.userName = userName;
    this.email = email.toLowerCase();
    this.passwordHashPromise = hashPassword(plainPassword);
  }
}

and to access it, you'd then do

const user = new User("foo", "bar", "[email protected]");
const passHash = await user.passwordHashPromise;

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.