Async/Await Performance Implications

It seems like every time I come across a explanation of Async/Await in JavaScript, the author cautions against using it because it ‘can slow things down because your code has to wait for the await call to finish before it can move forward’ or something to that effect.

That statement has always made me nervous. I’m at a point in my current project where it would be really convenient to use async/await, but I didn’t want to do that if it was going to result in my entire application freezing for a second or two while the await statement is resolved.

That seemed very unlikely, but I decided it was time to do some experimenting to make sure that I understood just exactly where the boundry of the performance hit started and ended.

Here is my first experiment:

const { Resolver } = require('dns').promises;
const resolver = new Resolver();

const ip = '204.79.197.212';

const resolveDNS = async (ip) => {
    try {
        let hostname = await resolver.reverse(ip);
        console.log(hostname);
    } catch (error) {
        console.log("Error: " + error);
    }
    
}

resolveDNS(ip);
console.log("Hey there.")

The code is just using the built-in NodeJS dns library to do a reverse lookup on an ip address that corresponds to hotmail.com.

If execution really froze the way some of those internet posts seemed to imply it might be doing, then the output would have been the hostname, followed by the “Hey there.” console.log.

Actual Output:
Hey there.
[ ‘a-0010.a-msedge.net’ ]

That’s very good, because it means that ‘everything’ doesn’t freeze up while waiting for the reverse dns call to resolve. Instead, it looks like once the code hits the await statement, it just shifts that function onto the event table where it waits until the dns query resolves, at which point it’s placed onto the event queue.

In the meantime though, the program is free to continue on executing unrelated pieces of code while it waits.

I next ran this experiment:

const { Resolver } = require('dns').promises;
const resolver = new Resolver();

const ip = '204.79.197.212';
const ip2 = '204.79.197.211'

const resolveDNS = async (ip, ip2) => {
    try {
        let hostname = await resolver.reverse(ip);
        console.log(hostname);
        let hostname2 = await resolver.reverse(ip2);
        console.log(hostname2); 
    } catch (error) {
        console.log("Error: " + error);
    }
    
}

for(let i=0; i<5; i++) {
    resolveDNS(ip, ip2);
}

console.log("Hey there.")

Actual Output:
Hey there.
[ 'a-0010.a-msedge.net' ]
[ 'a-0010.a-msedge.net' ]
[ 'a-0010.a-msedge.net' ]
[ 'a-0010.a-msedge.net' ]
[ 'a-0010.a-msedge.net' ]
[ 'a-0009.a-msedge.net' ]
[ 'a-0009.a-msedge.net' ]
[ 'a-0009.a-msedge.net' ]
[ 'a-0009.a-msedge.net' ]
[ 'a-0009.a-msedge.net' ]

Again, it the "Hey there." statement is being run before any of the dns queries are resolved. It's interesting though that all of the '0010' queries are resolving before any of the '0009' queries show up.

That indicates to me that the entire stack isn't being shifted onto the event table when execution hits the 'await'. Rather, just the 'async' function is being shifted to the event table, which allows the for loop to continue to the next iteration.

So we load up the event table with 5 calls to resolve the first ip address. Then we log out the "Hey there." statement. Then the first ip query resolves, and the async function is moved down to the event queue and then onto the call stack.

That first lookup (a '0010') is printed, and then the code hits the second 'await' line and that function is moved back onto the event table to wait while the query on the second ip address is resolved.

By that point, the other 4 queries on the first ip address have all resolved and been shifted down to the event queue and then into the call stack in turn.

They each then get to the second 'await' statement and get shifted back onto the event table to wait for the resolution on the lookup for the second ip address.

At this point, the "Hey there." has been logged, and the 5 '0010' lookups have also been logged to the console. Shortly after that, events are registered indicating that the queries for the second ip address are coming back, and each of those functions are shifted down to the event queue and then onto the call stack so that the '0009' statements can be logged.

So, with those two code snippets we've proved that when code hits an 'await' statement, it doesn't freeze all execution, just execution of the async function containing the 'await' statement. I'm already over my word-count target for this blog post, so next week I'll expound on what all of this means and where it's safe to use async/await, and where doing so will potentially result in a performance hit.

ES6 Spread Operator

One of the frustrations of being a ServiceNow developer is the fact that you don’t get to use all of the latest Javascript bells and whistles because the Rhino engine simple doesn’t support them.

It would be less of an issue if I was happy to live completely inside of the ServiceNow ecosystem, but I have things that I want to build outside of ServiceNow, so I’m faced with having to mentally switch back and forth from ‘old’ JavaScript to ‘new’ JavaScript.

One of the things that I’m trying to remember to use more often outside of my ServiceNow projects is the ES6 Spread Operator. I recently had a use case where the spread operator was perfect:

I was building an express route that was creating a new record in the many side of a one-to-many relationship, and since I had authentication in place that confirmed that the user was who they said they were, I didn’t want to make the user pass in their user id so that I could tie the new entry to the user (the ‘one’ side of the one-to-many relationship).

— Without Spread Operator —
// Create a new item
router.post(‘/items’,auth,async(req,res)=>{
    const item=new Item({
        name: req.body.name,
       description: req.body.description,
       weight: req.body.weight,
        owner:req.user._id
    })
    try{
        await item.save();
        res.status(201).send(item);
    }catch(error) {
        res.status(400).send(error);
    }
})

— With Spread Operator —

// Create a new item
router.post(‘/items’,auth,async(req,res)=>{
    const item=new Item({
        …req.body,
        owner:req.user._id
    })
    try{
        await item.save();
        res.status(201).send(item);
    }catch(error) {
        res.status(400).send(error);
    }
})

 

Without the spread operator, I have to manually assign each of the request body items that I want to be placed into the object that is ultimately saved as a new record or document in the database.

With the spread operator, I can just tell the system to break the request body into its component parts and then place those parts into my new object.

Using the spread operator is obviously less typing than the alternative. That isn’t a massive deal when it comes to a request that only has 3 key-value pairs, but could become a big deal on a much larger request.

More importantly, though is the fact that using the spread operator future proofs your code much better than the alternative. If I were to go back and add something to my items table, and the corresponding requests coming into my route, the spread operator will just grab that new data seamlessly and start saving it to the database. I don’t have to remember to come back and modify that route to allow for the inclusion of that new data.

You could have a problem with the spread operator resulting in user input that you weren’t expecting being saved into your database, so you’ll want to do some testing if you’re using the spread operator on something that is user input.

In my case, I confirmed that Mongoose is trimming off anything that isn’t established as part of my model for the collection, which means that the spread operator was the perfect solution for this problem.