DEV Community

Christian Tooley
Christian Tooley

Posted on

How to do Async express routes!

Overview

I recently took a job at a product development company focusing on Back-end Development! Part of the reality of working here is having to learn new technologies pretty often to fit whatever use-case we are going for. Recently we've been doing lots of web development using a combination of React & Node.js. Working in a JavaScript ecosystem is great, since we don't need to worry about parsing data ever and the Node environment is lightning fast, but we have to make sure that EVERYTHING is Asynchronous. 

Developing in an asynchronous manner was a struggle at first, if you search online, you'll find many different options with various pros & cons. We tried three different setups in the span of about a month before finally coming to a point where we didn't want to bang our heads on the table when we had to develop or modify complex business logic. Hopefully today I'll be able to pass on some of the information that we learned so that you can hit the ground running!

Our options

When I first arrived at work and starting developing, this is what our initial code looked like:

Nested callbacks

router.post("/store/update", (req, res) => {
  var someQuery = "CALL GetStore(?);";

  sql.query(someQuery, [res.query.storeID], function(result, error) {
    var store = result[0][0];

    //If something is true, do another query
    if (thing) {
      var otherQuery = "CALL UpdateStore(?,?,?);";

      sql.query(otherQuery, [req.query.storeID, field1, field2], function(result, error) {
        //blah blah, nested queries
        res.status(200).send();
      });
    }
  });
});
Enter fullscreen mode Exit fullscreen mode

You get the idea right? Lots of nesting in callbacks anytime we wanted to get some data, and then do other things on top of it. I hated it. To be fair I wrote most of this, and it wasn't really an issue till you get to 2+ levels of indention and need to change things. It gets especially tough when you want to add conditional queries as there is only one path of execution and its gotta be straight down the nesting tree.

After my frustration with nested callbacks had boiled up to an unimaginable level I had to change something! I started reading about promises online and saw the potential and got the go-ahead from my boss to convert our now 1-2 month old application to promise based.

The Next Step: Promises

router.post("/store/update", (req, res) => {
  var someQuery = "CALL GetStore(?);";

  sql.query(someQuery, [res.query.storeID])
  .then((result) => {
    var store = result[0][0];

    //If something is true, do another query
    if (thing) {
      var otherQuery = "CALL UpdateStore(?,?,?);";

      return sql.query(otherQuery, [req.query.storeID, field1, field2]);
    }

    return;
  }).then((result) => {
    //do something here with the result of otherQuery
    res.status(200).send();
  }).catch((error) => {
    //Handle any error that occured in either of the sql queries
    console.log(error);
  });
});
Enter fullscreen mode Exit fullscreen mode

This was a little bit better. Promises enabled us to turn our horizontal problem (chains getting super wide) into a vertical issue. Still annoying after the first few days. It again like callbacks, would get really complicated and nearly impossible to write if you needed to add conditional Async requests.

Our Savior: Async/Await syntax

So for us, promises were a big step up from callbacks, but as you can tell there was still lots of extra syntax and chaining that was really not needed. Async/Await syntax lets us get past that super easily. It still requires promise based functions, so for the previous example as well as this one, our SQL query function would be defined like so:

async function query(query, values) {
    return new Promise(async(resolve, reject) => {
        conn.query(query, values, function (error, results) {
            if (error) {
                reject(error);
            }

            resolve(results);
        });
    });
}
Enter fullscreen mode Exit fullscreen mode

And switching to Async/Await syntax gives us this lovely, clean, and synchronous looking code:

router.post("/store/update", (req, res) => {
  var someQuery = "CALL GetStore(?);";

  var someQueryResult = await sql.query(someQuery, [res.query.storeID]);

  var store = someQueryResult[0][0];

  //If something is true, do another query
  if (thing) {
    var otherQuery = "CALL UpdateStore(?,?,?);";

    var otherQueryResult = await sql.query(otherQuery, [req.query.storeID, field1, field2]);

    //do something here
  }

  res.status(200).send();
});
Enter fullscreen mode Exit fullscreen mode

Ding Ding Ding!!!! We have a winner. I love it. Clean, functional and no nesting!

Our only last issue with this situation revolves around error handling. With promises, you get your result in a .then() if there is one, and you get the error in the .catch() if there is one. But for async/await syntax the await part takes care of our .then(), but we still need a way to catch any errors so that our app doesn't crash. The following is an option:

var someQueryResult = await sql.query(someQuery, [res.query.storeID]).catch((error) => {
    console.log(error);
  });
Enter fullscreen mode Exit fullscreen mode

But I am not the biggest fan of this, it adds a lot more code that we need to write for each function, and generally our queries should not return errors, just data (empty or not). So we decided to only catch errors when we absolutely need to, to be able to do this we turned to a cool little library called express-async-handler. What this async handler lets us do is turn every single one of our express routes into a promise, so that we can do async/await syntax inside of them, and if any error happens it would be caught with a .catch() and passed to a error handler that we define. It looks like this:

const ash = require('express-async-handler');
router.post("/store/update", ash((req, res) => {
  var someQuery = "CALL GetStore(?);";

  var someQueryResult = await sql.query(someQuery, [res.query.storeID]);

  var store = someQueryResult[0][0];

  //If something is true, do another query
  if (thing) {
    var otherQuery = "CALL UpdateStore(?,?,?);";

    var otherQueryResult = await sql.query(otherQuery, [req.query.storeID, field1, field2]);

    //do something here
  }

  res.status(200).send()
}));
Enter fullscreen mode Exit fullscreen mode

and our error handler:

app.use((error, req, res, next) => {
    var status = error.status || 500;

    res.status(status).json({
        message: error.message
    });

    if(status != 500) {
        console.log('We have encounted an error: ' + error.message +  ' - ' + error.status);
        console.log(error.stack);
    }
});
Enter fullscreen mode Exit fullscreen mode

This is awesome! It allows us to ignore errors generally and we know if they happen they get caught, logged and return a 500 to the request (which fits logically.) To put the icing on the cake, we can also using this create our own errors if we want using
a npm package called http-errors. This is really just a little object that we can pass our status and message to that gets used in the error handler.

const Error = require('http-errors');

router.post("/store/update", ash((req, res) => {
  var someQuery = "CALL GetStore(?);";

  var someQueryResult = await sql.query(someQuery, [res.query.storeID]).catch((error) => {
    console.log(error);
  });

  var store = someQueryResult[0][0];

  if (store == undefined || store == null) {
    throw Error(200, 'That store object does not exist.');
  }

  //If something is true, do another query
  if (thing) {
    var otherQuery = "CALL UpdateStore(?,?,?);";

    var otherQueryResult = await sql.query(otherQuery, [req.query.storeID, field1, field2]);

    //do something here
  }

  res.status(200).send()
}));
Enter fullscreen mode Exit fullscreen mode

Well that is it! After the switch to Async/Await syntax and adding the async handler & error catcher, we were able to make our code super clean and easy to work with. It is no longer a huge pain to go back and update routes to add new logic, and will aid the readability of our code in the future.

Thank you for reading and I hope I was able to help even just a little bit!

Top comments (0)