DEV Community

Cover image for Cleaner async-await for asynchronous JavaScript
Siddharth Venkatesh
Siddharth Venkatesh

Posted on • Edited on

Cleaner async-await for asynchronous JavaScript

So, there are thousands of articles floating around the internet about why callbacks are bad and you should be using Promises and async/await. As the popular saying goes, the answer to most of the opinions in world of programming is "It depends". There is no one right solution for any problem.

What I'm addressing here is a very simple problem. I need to run multiple async operations in a function, and I need the code to look clean and readable. I have a handler function of a POST request to create a new product. It is written in Express and does the following

const createProduct = (req, res, next) => {
    // Check if the user is valid
    // Check if the product name already exists
    // Check if the store exists
    // Save product to database
}
Enter fullscreen mode Exit fullscreen mode

Promise based approach

A promise based approach would look something like this

const createProduct = (req, res, next) => {
    const { id: userId } = req.user;
    User.findOne({id: userId})
        .then((user) => {
            if (!user) {
                console.log('User does not exist');
                return res.status(400).json({
                    status: 'error',
                    message: 'User does not exist',
                });
            }
            const { name, storeId, price } = req.body;
            Product.findOne({name})
                .then((product) => {
                    if (product) {
                        console.log('Product with the same name already exists');
                        return res.status(400).json({
                            status: 'error',
                            message: 'Product with the same name already exists',
                        });
                    }
                    Store.findOne({id: storeId})
                        .then((store) => {
                            if (!store) {
                                console.log('Store does not exist');
                                return res.status(400).json({
                                    status: 'error',
                                    message: 'Store does not exist',
                                })
                            }
                            // Valid product. Can be saved to db
                            const newProduct = new Product({
                                name,
                                storeId,
                                price,
                            });
                            newProduct.save()
                                .then(() => {
                                    console.log('Product saved successfully');
                                    return res.status(200).json({
                                        status: 'success',
                                        message: 'Product saved successfully',
                                    });
                                })
                                .catch((err) => {
                                    console.log('err');
                                    next(err);
                                })
                        })
                        .catch((err) => {
                            console.log(err);
                            next(err);
                        })
                })
                .catch((err) => {
                    console.log(err);
                    next(err);
                })
        })
        .catch((err) => {
            console.log(err);
            next(err);
        })
}
Enter fullscreen mode Exit fullscreen mode

Async-await based approach

And if you convert this to a async await based approach, you would end up with something very similar.

const createProduct = async (req, res, next) => {
    const { id: userId } = req.user;
    try {
        const user = await User.findOne({id: userId});
        if (!user) {
            console.log('User does not exist');
            return res.status(400).json({
                status: 'error',
                message: 'User does not exist',
            });
        }
        const { name, storeId, price } = req.body;
        try {
            const product = await Product.findOne({name});
            if (product) {
                console.log('Product with the same name already exists');
                return res.status(400).json({
                    status: 'error',
                    message: 'Product with the same name already exists',
                });
            }
            try {
                const store = await Store.findOne({id: storeId});
                if (!store) {
                    console.log('Store does not exist');
                    return res.status(400).json({
                        status: 'error',
                        message: 'Store does not exist',
                    })
                }
                try {
                    const newProduct = new Product({
                        name,
                        storeId,
                        price,
                    });
                    await newProduct.save();
                    console.log('Product saved successfully');
                    return res.status(200).json({
                        status: 'success',
                        message: 'Product saved successfully',
                    });
                } catch (err) {
                    console.log('Error when saving product', err);
                    next(err);
                }
            } catch (err) {
                console.log('Error when fetching store', err);
                next(err);
            }
        } catch (err) {
            console.log('Error when fetching product', err);
            next(err);
        }
    } catch (err) {
        console.log('Error when fetching user', err);
        next(err);
    }
}
Enter fullscreen mode Exit fullscreen mode

There is nothing wrong with this approach and works pretty well for small functions. But when the number of async operations increase, the code goes into this pyramid structure which is hard to understand. Usually called the Pyramid of doom.

Linear async-await

To overcome this and to give our code a linear structure, we can write a utility function which fires the promise and returns the error and success states.

const firePromise = (promise) => {
    return promise
        .then((data) => {
            return [null, data];
        })
        .catch((err) => {
            return [err, null];
        })
}
Enter fullscreen mode Exit fullscreen mode

We can pass any async operation which returns a promise to this function and it will give us error and success states in an array. Goes something like this.

const [error, user] = await firePromise(User.findOne({id: userId}));
Enter fullscreen mode Exit fullscreen mode

Now we can refactor our createProduct handler to use our firePromise function.

const createProduct = async (req, res, next) => {
    let error, user, product, store;
    const { id: userId } = req.user;
    try {
        [error, user] = await firePromise(User.findOne({id: userId}));
        if(error) {
            console.log('Error when fetching user', error);
            next(error);
        }
        if(!user) {
            console.log('User does not exist');
            return res.status(400).json({
                status: 'error',
                message: 'User does not exist',
            });
        }
        const { name, storeId, price } = req.body;
        [error, product] = await firePromise(Product.findOne({name}));
        if(error) {
            console.log('Error when fetching product', error);
            next(error);
        }
        if (product) {
            console.log('Product with the same name already exists');
            return res.status(400).json({
                status: 'error',
                message: 'Product with the same name already exists',
            });
        }
        [error, store] = await firePromise(Store.findOne({id: storeId}));
        if(error) {
            console.log('Error when fetching store', error);
            next(error);
        }
        if (!store) {
            console.log('Store does not exist');
            return res.status(400).json({
                status: 'error',
                message: 'Store does not exist',
            })
        }
        const newProduct = new Product({
            name,
            storeId,
            price,
        });
        [error] = await firePromise(newProduct.save());
        if (error) {
            console.log('Error when saving product', err);
            next(error);
        }
        console.log('Product saved successfully');
        return res.status(200).json({
            status: 'success',
            message: 'Product saved successfully',
        });
    } catch (err) {
        console.log('Unexpected error');
        next(err);
    }
}
Enter fullscreen mode Exit fullscreen mode

In my opinion, this is much more readable because of its linear structure. This function can be used with any JS framework to write readable and maintainable async code.

This was inspired by await-to-js library, and I use it in almost all my JS projects. Go give them a star.

Cheers!

Top comments (0)