CoderLee
CoderLee

Reputation: 3479

async await being skipped over in TypeScript API

I have an async method in a controller that sets a results object. The problem is instead of waiting for the await to finish executing my code is jumping to the response object call which leaves the needed variable undefined. In the debugger breakpoints in the method that's supposed to be executed are hit after the undefined error. Can anyone explain why async await isn't working here?

method in controller class:

public async loginUser(req: Request, res: Response) {
    const { name, password } = req.body;
    let result: ILoginResult = await UserData.login(name, password); // always undefined
    res.status(result.status).send(result.result); // gets hit before result is set
  }

UserData class:

import bcrypt from 'bcrypt';
import jwt from 'jsonwebtoken';
import mongoose from 'mongoose';
import ILoginResult from './ILoginResult';
import UserModel from '../../models/UserModel';

class UserData {
    private connUri: string;

    constructor() {
        this.connUri = process.env.MONGO_LOCAL_CONN_URL;
    }

    public async login(name: string, password: string) {

        try {
            await mongoose.connect(this.connUri, { useNewUrlParser: true, useUnifiedTopology: true, useCreateIndex: true, }, (err) => {
                let result: ILoginResult = { status: 0, result: null, error: '', token: '' };
                let status = 200;
                if (!err) {
                    UserModel.findOne({ name }, (err, user) => {
                        if (!err && user) {
                            // We could compare passwords in our model instead of below as well
                            bcrypt.compare(password, user.password).then(match => {
                                if (match) {
                                    status = 200;
                                    // Create a token
                                    const payload = { user: user.name };
                                    const options = { expiresIn: '2d', issuer: 'http://localhost' };
                                    const secret = process.env.JWT_SECRET;
                                    const token = jwt.sign(payload, secret, options);

                                    // console.log('TOKEN', token);
                                    result.token = token;
                                    result.status = status;
                                    result.result = user;
                                } else {
                                    status = 401;
                                    result.status = status;
                                    result.error = `Authentication error`;
                                }
                                return result;
                            }).catch(err => {
                                status = 500;
                                result.status = status;
                                result.error = err;
                                return { status: status, result: result };
                            });
                        } else {
                            status = 404;
                            result.status = status;
                            result.error = err;
                            return result;
                        }
                    });
                } else {
                    status = 500;
                    result.status = status;
                    result.error = err.toString();
                    return result;
                }
            });
        } catch (e) {
            let result: ILoginResult;
            result.error = e.toString();
            result.status = 500;
            return result;
        }
    }
}

export default new UserData();

Upvotes: 0

Views: 1256

Answers (2)

Klaycon
Klaycon

Reputation: 11080

await doesn't work on traditional node-style callbacks. It only works on Promises. Since you're providing a callback, the callback will be executed asynchronously and not waited for by the await.

Besides, using a callback when you're already using await and the function returns a promise really defeats the purpose of even using await. Write the code without callbacks:

        let result: ILoginResult = { status: 0, result: null, error: '', token: '' };
        try {
            await mongoose.connect(this.connUri, { useNewUrlParser: true, useUnifiedTopology: true, useCreateIndex: true, });
            let status = 200;
            let user = await UserModel.findOne({ name });
            if (user) {
                // We could compare passwords in our model instead of below as well
                let match = await bcrypt.compare(password, user.password);
                if (match) {
                    status = 200;
                    // Create a token
                    const payload = { user: user.name };
                    const options = { expiresIn: '2d', issuer: 'http://localhost' };
                    const secret = process.env.JWT_SECRET;
                    const token = jwt.sign(payload, secret, options);

                    // console.log('TOKEN', token);
                    result.token = token;
                    result.status = status;
                    result.result = user;
                } else {
                    status = 401;
                    result.status = status;
                    result.error = `Authentication error`;
                }
                return result;
            } else {
                status = 404;
                result.status = 404;
                result.error = err;
                return result;
            }
        } catch (e) {
            result.error = e.toString();
            result.status = 500;
            return result;
        }

To me this is way cleaner, less of the callback trees from hell and more elegant usage of async/await. Since the whole thing is wrapped in try/catch, if any of the await ...s error, the outer catch will grab it and return your standard error 500 with message instead of needing to repeat that code many times within.

Upvotes: 1

T.J. Crowder
T.J. Crowder

Reputation: 1075517

Don't mix async/await directly with callback-based APIs.

According to the documentation, mongoose.connect does return a promise (provided you're using an up-to-date version), but there's nothing to suggest it makes that promise wait for things happening in the callback you give it.

Instead, do those things in the code following the await mongoose.connect.

Something along these lines (in UserData):

public async login(name: string, password: string) {
    let result: ILoginResult = { status: 0, result: null, error: '', token: '' };
    try {
        await mongoose.connect(this.connUri, { useNewUrlParser: true, useUnifiedTopology: true, useCreateIndex: true, });
        const user = await UserModel.findOne({ name });
        let status = 200;
        let match = await bcrypt.compare(password, user.password).catch(() => false);
        if (!match) {
            status = 401;
            result.status = status;
            result.error = `Authentication error`;
            return result;
        }
        status = 200;
        // Create a token
        const payload = { user: user.name };
        const options = { expiresIn: '2d', issuer: 'http://localhost' };
        const secret = process.env.JWT_SECRET;
        const token = jwt.sign(payload, secret, options);

        // console.log('TOKEN', token);
        result.token = token;
        result.status = status;
        result.result = user;
        return result;
    } catch (e) {
        result.error = e.toString();
        result.status = 500;
        return result;
    }
}

Upvotes: 1

Related Questions