Why isn't this unit test catching an error from this async/await function?

When you're writing unit tests for asynchronous functions in JavaScript, one test case you'll usually want to have is making sure the async function throws an error in case of an error scenario.

Let's imagine writing a test for an item function that calls a database and returns an item:

const fetchItem = async function (itemName) {
  if (typeof itemName !== 'string') {
    throw new Error('argument should be a string')
  } else {
    return await db.select(itemName)
  }
}

module.exports = {
  fetchItem
}

Note: normally I don't like doing type checks on arguments, but this is easy for demonstration purposes.

A reasonable unit test for this might look like:

const { fetchItem } = require('../path/to/fn')

describe('#fetchItem', () => {
  it('should catch an error', async () => {
    await expect(fetchItem(3)).to.eventually.throw()
  })
})

In this case, we call the fetchItem() function with an argument that is not a string (which our database query will expect). It's an async function so we await it and expect it to eventually throw, since the function will throw a new Error if passed a non-string argument.

It seems like it should pass, right?

Then why does the test fail with an uncaught error? Why does the error just show up in the console without the test passing?

Let's take a look at why it's not working, and how to fix it...

Why doesn't it work like you expect it to?

The beauty of async/await is that it makes asynchronous code read as if it were synchronous code. So synchronous that it can be easy to forget you're still dealing with async code.

It's important to remember that in JavaScript whenever you have a function with the async keyword, it always returns a Promise. And when you have a function that returns a Promise it's either resolved or rejected.

When we throw that error like we did in the fetchItem() function,

if (typeof itemName !== 'string') {
    throw new Error('argument should be a string')
}

it's really rejecting the Promise. It will reject with an error, but it's a rejected Promise, nonetheless.

The fix

The fix for this is very simple. Import chai-as-promised into your tests like so:

const chai = require('chai')
const chaiAsPromised = require('chai-as-promised');

const expect = chai.expect
chai.use(chaiAsPromised)

Then change the test to be:

describe('#fetchItem', () => {
  it('should catch an error', async () => {
    await expect(fetchItem(3)).to.be.rejected
  })
})

All that changed was instead of to.eventually.throw(), it becomes to.be.rejected. If you want to test to make sure it's rejected with the right error message, you can change it to to.be.rejectedWith('argument should be a string').

A note on return vs await

Chai will wait for Promises, so instead of using await

await expect(fetchItem(3)).to.be.rejected

you could use return

return expect(fetchItem(3)).to.be.rejected

I prefer to use await as it reminds me that I'm working with an async function, but this is worth pointing out in case you find other examples using return.

Wrapping up

With native Promises, where you explicitly reject the Promise when you hit an error scenario, it's a bit easier to remember that you're testing for a rejected Promise, not a caught error.

I've written plenty of working tests for async/await functions that throw errors, but it's still an easy thing to forget. I encountered it recently when I was writing the code for the post on scenarios for unit testing Node services, which involved a lot of asynchronous code. And by the way, if you're looking for a list of common tests you should have for Node services, definitely check out that post.

I think testing should be as easy as possible in order to remove the barriers to actually writing them. It's one thing to get stuck on code - you don't have any choice but to fix it. But its another thing to get stuck on tests - with tests you technically can skip them.

I'm trying to make testing and other things in JavaScript easier by sending out tutorials, cheatsheets, and links to other developers' great content. Sign up below to get on my mailing list!

Subscribe for more testing and JavaScript content!

No spam ever. Unsubscribe any time.