Verify that an exception is thrown using Mocha / Chai and async/await

Viewed 61310

I'm struggling to work out the best way to verify that a promise is rejected in a Mocha test while using async/await.

Here's an example that works, but I dislike that should.be.rejectedWith returns a promise that needs to be returned from the test function to be evaluated properly. Using async/await removes this requirement for testing values (as I do for the result of wins() below), and I feel that it is likely that I will forget the return statement at some point, in which case the test will always pass.

// Always succeeds
function wins() {
  return new Promise(function(resolve, reject) {
    resolve('Winner');
  });
}

// Always fails with an error
function fails() {
  return new Promise(function(resolve, reject) {
    reject('Contrived Error');
  });
}

it('throws an error', async () => {
  let r = await wins();
  r.should.equal('Winner');

  return fails().should.be.rejectedWith('Contrived Error');
});

It feels like it should be possible to use the fact that async/await translates rejections to exceptions and combine that with Chai's should.throw, but I haven't been able to determine the correct syntax.

Ideally this would work, but does not seem to:

it('throws an error', async () => {
  let r = await wins();
  r.should.equal('Winner');

  (await fails()).should.throw(Error);
});
14 Answers

I use a custom function like this:

const expectThrowsAsync = async (method, errorMessage) => {
  let error = null
  try {
    await method()
  }
  catch (err) {
    error = err
  }
  expect(error).to.be.an('Error')
  if (errorMessage) {
    expect(error.message).to.equal(errorMessage)
  }
}

and then, for a regular async function like:

const login = async (username, password) => {
  if (!username || !password) {
    throw new Error("Invalid username or password")
  }
  //await service.login(username, password)
}

I write the tests like this:

describe('login tests', () => {
  it('should throw validation error when not providing username or passsword', async () => {

    await expectThrowsAsync(() => login())
    await expectThrowsAsync(() => login(), "Invalid username or password")
    await expectThrowsAsync(() => login("username"))
    await expectThrowsAsync(() => login("username"), "Invalid username or password")
    await expectThrowsAsync(() => login(null, "password"))
    await expectThrowsAsync(() => login(null, "password"), "Invalid username or password")

    //login("username","password") will not throw an exception, so expectation will fail
    //await expectThrowsAsync(() => login("username", "password"))
  })
})
  1. Install chai-as-promised for chai (npm i chai-as-promised -D)
  2. Just call your promise, no await should be applied!
import chai from 'chai';
import chaiAsPromised from 'chai-as-promised';

chai.use(chaiAsPromised);

const expect = chai.expect;

describe('MY_DESCR', () => {
  it('MY_TEST', async () => {
    expect(myAsyncFunctionThatWillReject()).to.eventually.be.rejected; 
  });
});

You can use async/await and should for simple verification

it('should not throw an error', async () => {
  try {
    let r = await wins();
    r.should.equal('Winner');
  } catch (error) {
    error.should.be.null(); //should.not.exist(error) can also be used
  }
});

it('throws an error', async () => {
  let err;
  try {
    await fails();
  } catch (error) {
    err = error;
  }
    err.should.be.Error();
    err.should.have.value("message", "Contrived Error");
});

I have came with this solution:

import { assert, expect, use } from "chai";
import * as chaiAsPromised from "chai-as-promised";

describe("using chaiAsPromised", () => {
    it("throws an error", async () => {
        await expect(await fails()).to.eventually.be.rejected;
    });
});

If test your Promised function, in the test must wrap the code inside a try/catch and the expect() must be inside the catch error block

const loserFunc = function(...args) {
  return new Promise((resolve, rejected) => {
    // some code
    return rejected('fail because...');
  });
};

So, then in your test

it('it should failt to loserFunc', async function() {
  try {
    await loserFunc(param1, param2, ...);
  } catch(e) {
    expect(e).to.be.a('string');
    expect(e).to.be.equals('fail because...');
  }
});

That is my approach, don't know a better way.

This is my Solution for the problem .

    try {
        // here the function that i expect to will return an errror
        let walletid = await Network.submitTransaction(transaction)
    } catch (error) {
        //  assign error.message to ErrorMessage
        var ErrorMessage = error.message;
        //  catch it and  re throw it in assret.throws fn and pass the error.message as argument and assert it is the same message expected
        assert.throws(() => { throw new Error(ErrorMessage) },'This user already exists');
    }
    // here assert that ErrorMessage is Defined ; if it is not defined it means that no error occurs
    assert.isDefined(ErrorMessage);

A no dependency on anything but Mocha example.

Throw a known error, catch all errors, and only rethrow the known one.

  it('should throw an error', async () => {
    try {
      await myFunction()
      throw new Error('Expected error')
    } catch (e) {
      if (e.message && e.message === 'Expected error') throw e
    }
  })

If you test for errors often, wrap the code in a custom it function.

function itThrows(message, handler) {
  it(message, async () => {
    try {
      await handler()
      throw new Error('Expected error')
    } catch (e) {
      if (e.message && e.message === 'Expected error') throw e
    }
  })
}

Then use it like this:

  itThrows('should throw an error', async () => {
    await myFunction()
  })

you can write a function to swap resolve & reject handler, and do anything normally

const promise = new Promise((resolve, rejects) => {
    YourPromise.then(rejects, resolve);
})
const res = await promise;
res.should.be.an("error");

Another way (applicable to an async function, but not using await in the test) is calling done with an assertion error:

it('should throw Error', (done) => {
  myService.myAsyncMethod().catch((e) => {
    try {
      // if you want to check the error message for example
      assert.equal(e.message, 'expected error');
    } catch (assertionError) {
      done(assertionError); // this will fail properly the test
      return; // this prevents from calling twice done()
    }

    done();
  });
});

Add this at the top of your file:

import * as chai from 'chai';
import chaiAsPromised from 'chai-as-promised';

chai.use(chaiAsPromised)

Then the assert should be this way:

await expect(
    yourFunctionCallThatReturnsAnAwait()
).to.eventually.be.rejectedWith("revert"); // "revert" in case of web3

Here's a TypeScript implementation of solution:

import { expect } from 'chai';

export async function expectThrowsAsync(
  method: () => Promise<unknown>,
  errorMessage: string,
) {
  let error: Error;
  try {
    await method();
  } catch (err) {
    error = err;
  }
  expect(error).to.be.an(Error.name);
  if (errorMessage) {
    expect(error.message).to.equal(errorMessage);
  }
}

Inspired by solution from @kord

On Chai I get the error Property 'rejectedWith' does not exist on type 'Assertion' for the top answer. Below is a quick solution, chai-as-promised is probably better long term.

it('passes', async () => {
  wins().then((res) => { expect(res).to.eq('Winner') })
})

it('throws an error', async () => {
  fails().then(null, (err) => { expect(err).to.be.an('error') })
})
Related