Handling Promise rejections in Express.js (Node.js) with ease

Async/await API is a great step towards code readability in Node.js. But it also introduces one significant problem which a lot of developers struggle in day to day job, it's error handling and to be precise - promise rejection (or exception) in async code wrappers in Express.js framework. This post will show a way to handle it properly.

Express.js is a great framework that makes building REST API projects a breeze. Addition of async/await API to Node.js made the code clearer as opposed to so called "callback hell" symptom in Node.js apps. But the problem arises when you have to deal with promise rejections in you controllers. To put it the most simple way, you end up putting all of you controller's code into try-catch like this below:

router.route('/save-item').post(async (req, res) => {
    try {
        let data = await db.saveItem(req.body)
        res.json({
            status: true,
            data
        })
    } catch (e) {
        res.status(500).json({
            status: false,
            error: e.message
        })
    }

})

Can you see how much boilerplate code it produces? At least for every controller you have to implement two ways of sending a response. The one that handles the error and the other that handles success. In addition somewhere in your application this code below will appear for sure:

process.on('unhandledRejection', error => {
  console.log('unhandledRejection', error.message);
});

This is not an elegant solution as it has not attachment to Express.js webserver and therefor cannot properply format response and send it to client. As a result, the client ends up waiting for response infinitely.

There is an elegant solution though. Take a look at this code below:

let wrapRoute = async (req, res, next) => {
    try {
        // run controllers logic
        await fn(req, res, next)
    } catch (e) {
        // if an exception is raised, do not send any response
        // just continue performing the middleware chain
        next(e)
    }
}

// in your routes definition
router.route('/save-item').post(wrapRoute(async (req, res) => {
    let data = await db.saveItem(req.body)
    res.json({
        status: true,
        data
    })
}))

You see? All the repeated try-catch code is now eliminated with wrapping our controller's code with `wrapRoute` method which handles exceptions and promise rejections. There is just one more thing to be added.

app.use(function(err, req, res, next) {
    // If err has no specified error code, set error code to 'Internal Server Error (500)'
    if (!err.statusCode) {
        err.statusCode = 500;
    } 

    res.status(err.statusCode).json({
        status: false,
        error: err.message
    });

});

We have to add a middleware at the end of all of our route definitions so when the exception is raised then this middleware picks it and produces an error response.



Similar searches: Unhandled Promise Rejections in Node.js / What is an unhandled promise rejection? / Support catching rejected promises in middleware functions express js / Catching promise rejection in express.js middleware

These posts might be interesting for you:

  1. RabbitMQ cluster on Docker with Nodejs in 5 minute
  2. Using Cloud Run to run Chrome Headless on a budget
Author: Peter

I'm a backend programmer for over 10 years now, have hands on experience with Golang and Node.js as well as other technologies, DevOps and Architecture. I share my thoughts and knowledge on this blog.