J Cole Morrison
J Cole Morrison

J Cole Morrison

Developer Advocate @HashiCorp, DevOps Enthusiast, Startup Lover, Teaching at awsdevops.io



Complete Guides:

Async

5 Tips and Thoughts on Async / Await Functions

Posted by J Cole Morrison on .

5 Tips and Thoughts on Async / Await Functions

Posted by J Cole Morrison on .

5 Tips and Thoughts on Async/Await Functions

So I finally hopped on the boat with usage of ES7 Async/Await, though primarily when in Node.js land (as opposed to front-end, babelling, webpacking land). And, geez it is so refreshing to NOT have to tab and space my thumb and pinky to death.

After a good amount of usage, I wanted to address some nuances and common questions I had while transitioning over to using it a good amount:

Table of Contents

  1. Try/Catch
  2. Testing with Mocha, Sinon, and Chai
  3. Usage with Core Node Modules
  4. Usage with the AWS SDK
  5. When not to use?

1 - Try / Catch

When you first start using Async/Await all you see is the "let me sell you on it" version:

// Before Async/Await
const main = (paramsA, paramsB, paramsC, done) => {  
  funcA(paramsA, (err, resA) => {
    if (err) return done(err)

    return funcB(paramsB, (err, resB) => {
      if (err) return done(err)

      funcC(paramsC, (err, resC) => {
        if (err) return done(err)

        // (╯°□°)╯︵ ┻━┻

        return done(null, { resA, resB, resC })
      })
    })
  })
}
// Async/Await
const main = async (paramsA, paramsB, paramsC) => {  
  const resA = await funcA(paramsA)
  const resB = await funcB(paramsB)
  const resC = await funcC(paramsC)

  // \(T.T)/
  return { resA, resB, resC }
}

However, you soon realize that.. hey none of this accounts for errors. So then you start using try/catch after researching that Node 8.3+ has no more performance hits doing so. If you're really zealous your code might initially look something like...

// Async/Await + Try/Catch + Unique handling of each error
const main = async (paramsA, paramsB, paramsC) => {  
  let resA
  let resB
  let resC

  try {
    resA = await funcA(paramsA)
  } catch (error) {
    throw error
  }

  try {
    resB = await funcB(paramsB)
  } catch (error) {
    throw error
  }

  try {
    resC = await funcC(paramsC)
  } catch (error) {
    throw error
  }

  // (o.o;)
  return { resA, resB, resC }
}

You then realize that, unless you REALLY need to do something unique for each error you can do all of it in one try catch block. You can still even handle the errors uniquely assuming you want to dive into the actual error object.

// Async/Await + Try/Catch + Unique handling of each error
const main = async (paramsA, paramsB, paramsC) =>  
  try {
    const resA = await funcA(paramsA)
    const resB = await funcB(paramsB)
    const resC = await funcC(paramsC)

      // (^.^')
    return { resA, resB, resC }
  } catch (error) {
    throw error
  }
}

HOWEVER.

Upon returning to use your incredible new Async/Await function in the wild, you start encountering this type of error everywhere:

(node: xxx) UnhandledPromiseRejectionWarning: Unhandled promise rejection  (rejection id: y): Error: some sort of error
(node: xxx) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.

// (-.-;)

So now you've probably wasted at least 15 to 20 minutes of your life trying to figure out what's causing it. Simply put, you're throwing an error if it happens, but you're not 'catching' and responding to it.

The solution? Treat the call to your function as if it were a promise. Because calls to an Async/Await function return a promise.

So looking at our shiny Async/Await function here:

const main = async (paramsA, paramsB, paramsC) => {  
  try {
    const resA = await funcA(paramsA)
    const resB = await funcB(paramsB)
    const resC = await funcC(paramsC)

    return { resA, resB, resC }
  } catch (error) {
    // sure it's thrown, but who catches it??
    throw error
  }
}

// somewhere else...
main()  
  .then(d => { // do things with the result })
  .catch(e => { // handle that error! })

Simplifying Even More

Interestingly enough, after realizing that a call to an Async/Await function is just returning a promise, you realize you can get away WITHOUT using the try catch.

// Since each of the await calls will trigger the `.catch` if they fail...
const main = async (paramsA, paramsB, paramsC) => {  
  const resA = await funcA(paramsA)
  const resB = await funcB(paramsB)
  const resC = await funcC(paramsC)

  return { resA, resB, resC }
}

// ... all we need is this `.catch` to handle all of them.
main()  
  .then(d => { // do things with the result })
  .catch(e => { // handle that error! })

You'll notice that this lets us return the function to that "let me sell you on it" version!

Now of course this is assuming you don't have some absolute need to handle each error uniquely, right then and there in that code. But for many common errors, some catch all at the end is more than enough.

If you do need to uniquely handle an error and don't want to use try/catch you can also do this:

const main = async (paramsA, paramsB, paramsC) => {  
  const resA = await funcA(paramsA)
  const resB = await funcB(paramsB).catch(e => { // things unique to this error })
  const resC = await funcC(paramsC)

  return { resA, resB, resC }
}

// ... all we need is this `.catch` to handle all of them.
main()  
  .then(d => { // do things with the result })
  .catch(e => { // handle all other errors!! })

This allows you to handle the error for our funcB uniquely while still letting the other ones get handled generically.

2 - Testing with Mocha, Sinon, and Chai

Throughout the entire time of building the Async/Await function, you've been dreading that moment where you go to test it. In fact, maybe before reading this article you HAD already gone and tried to test it.

If you'd left your new function in a state peppered with try/catch and throw error statments, you probably also started throwing the async and await keywords all around the mocha tests. You've probably considered some additional promise library to handle these types. Because seeing UnhandledPromiseRejectionWarning in your tests now is starting to drive you nuts.

Well don't. Testing them is actually VERY straight-forward as long as you keep three things in mind:

1) Don't try and mix in async functions with callbacks AND promises.

If you have an async function inside of a function that expects to have a callback called i.e....

const thing = (params, done) => {  
  ApiCall(params, async (err, data) => {
    if (err) return done(err)

    const things = await OtherApiCall(data)

    return done(null, things)
  })
}

... your day is going to be ruined.

2) Remember that calls to an Async/Await Function RETURN A PROMISE

I keep saying that, because I kept reading it and ignoring/forgetting it.

3) If you return a promise to a Mocha test case, it'll wait on it.

So let's look at how one can easily test an Async/Await function in Mocha without all sorts of 3rd party libraries and the like:

The Main File:

// main.js
const main = async (paramsA, paramsB, paramsC) => {  
  const resA = await apiA.create(paramsA)
  const resB = await apiB.delete(paramsB)
  const resC = await apiC.update(paramsC)

  return { resA, resB, resC }
}

The Test File:

// test.js
const expect = require('chai').expect  
const sinon = require('sinon')  
const main = require('main.js')  
const apiA = require('apiA')  
const apiB = require('apiB')  
const apiC = require('apiC')

describe('Main Function', () => {  
  let apiAstub
  let apiBstub
  let apiCstub

  beforeEach(() => {
    apiAstub = sinon.stub(apiA, 'create')
    apiBstub = sinon.stub(apiB, 'delete')
    apiCstub = sinon.stub(apiC, 'update')
  })

  afterEach(() => {
    apiAstub.restore()
    apiBstub.restore()
    apiCstub.restore()
  })
  it('should handle errors if apiA.create() fails', () => {
    apiAstub.throws('error for apiA.create()')

    // Because a call to main returns a promise, we handle it with a catch
    return main('a', 'b', 'c').catch((e) => {

      // mocha will wait for the promise to resolve or throw
      expect(e).to.equal('error for apiA.create()')
    })
  })
  it('should handle errors if apiB.delete() fails', () => {
    apiAstub.returns('success a')
    apiBstub.throws('error for apiB.delete()')

    return main('a', 'b', 'c').catch((e) => {
      expect(e).to.equal('error for apiB.create()')
    })
  })
  it('should handle errors if apiC.update() fails', () => {
    apiAstub.returns('success a')
    apiBstub.returns('success b')
    apiCstub.throws('error for apiC.delete()')

    return main('a', 'b', 'c').catch((e) => {
      expect(e).to.equal('error for apiC.create()')
    }) 
  })
  it('should return the responses of all functions if all api calls succeed', () => {
    apiAstub.returns('success a')
    apiBstub.returns('success b')
    apiCstub.throws('success c')

    return main('a', 'b', 'c').then((res) => {
      expect(res).to.deep.equal({
        resA: 'success a',
        resB: 'success b',
        resC: 'success c',
      })
    }) 
  })
})

A quick note here:

In order to get those await functions to follow control flow we use returns and throws. If you've been doing lots of testing you may assume that yields is involved. You won't need that if you're stubbing out your API calls even though the await keyword is being used (which in unit testing a singular function you should be).

3 - Usage with Core Node Modules

What if you want to use something like fs.readFile? If you've tried to do the following...

const fs = require('fs')

async function readThings () {  
  const file = await fs.readFile('./file.txt', 'utf8')
  // Nope
  return file
}

... you'll be initially disappointed.

The problem here is that readFile isn't in a promised based form. However, Node 8's util module provides us with a method called promisify. We can use this to turn modules functions like readFile into Async/Await ready functions! An example:

const fs = require('fs')  
const { promisify } = require('util')  
const readFile = promisify(fs.readFile)

async function readThings () {  
  const file = await readFile('./file.txt', 'utf8')
  // Success!
  return file
}

And there ya have it. Now you can async and await even more.

4 - Usage with the AWS SDK

If you work with the AWS SDK a good amount, and are still on a async/await crusade, you may be burning to use it here as well. And if you try something like the following:

const aws = require('aws-sdk')

async function getEc2Info () {  
  const ec2 = new aws.EC2()
  const instances = await ec2.describeInstances()

  // do things with instances
}

...You'll be shattered to find that it doesn't work. After which, you might even be tempted to use the util.promisify. And once again you'll be shattered. If you look up about how to do so, you'll find all of these announcements and articles saying that it now supports promises.

Well my friend, you were so close. All you need to do to work with async/await with the AWS SDK is to add .promise() to the calls like so:

const aws = require('aws-sdk')

async function getEc2Info () {  
  const ec2 = new aws.EC2()
  const instances = await ec2.describeInstances().promise() // <--

  // Actually do things with instances!
}

5 - When not to use?

This is surpringly simple and yet difficult to catch when/if we fall down the rabbit hole of upgrading. Async/Await is good to go as of Node 8. With Node 8 being in LTS the fear of being too far ahead of the curve with it's usage is more or less dispelled.

However, not all modules, libraries, frameworks, and patterns work with it right out of the box. Including your own code.

It's very tempting to change that one function over to async/await. And that leads to its tests breaking. And that leads to other functions it uses that expect it as callback to break. And then you're tempted to change the functions that it breaks... and this continues on until you're at the github issues section of your main framework complaining about how to get it to work properly with promises and async/await.

So IF you can catch yourself before going down that long winding rabbit hole, that only a well timed git stash will save you from, THAT is when not to use it.


More from the blog

J Cole Morrison

J Cole Morrison

http://start.jcolemorrison.com

Developer Advocate @HashiCorp, DevOps Enthusiast, Startup Lover, Teaching at awsdevops.io

View Comments...
J Cole Morrison

J Cole Morrison

Developer Advocate @HashiCorp, DevOps Enthusiast, Startup Lover, Teaching at awsdevops.io



Complete Guides: