36

Is it possible to write a test using truffle that attempts to confirm that a throw occurs in a contract? For example, if I had a contract with function...

contract TestContract {
  function testThrow() {
    throw;
  }
}

if I write a test in truffle that invokes this function, then truffle test basically crashes with:

Error: VM Exception while executing transaction: invalid JUMP

Is there any way to handle this exception from within your test to verify that the throw actually occurred? The reason I want to do so is to test that my functions actually throw when the user passes in invalid input?

10 Answers 10

18

You can use OpenZeppelin's expectThrow helper -

Source: https://github.com/OpenZeppelin/zeppelin-solidity/blob/master/test/helpers/expectThrow.js

export default async promise => {
      try {
        await promise;
      } catch (error) {
        // TODO: Check jump destination to destinguish between a throw
        //       and an actual invalid jump.
        const invalidJump = error.message.search('invalid JUMP') >= 0;
        // TODO: When we contract A calls contract B, and B throws, instead
        //       of an 'invalid jump', we get an 'out of gas' error. How do
        //       we distinguish this from an actual out of gas event? (The
        //       testrpc log actually show an 'invalid jump' event.)
        const outOfGas = error.message.search('out of gas') >= 0;
        assert(
          invalidJump || outOfGas,
          "Expected throw, got '" + error + "' instead",
        );
        return;
      }
      assert.fail('Expected throw not received');
    };

I use it my test cases like this -

import expectThrow from './helpers/expectThrow';
.
.
.
describe('borrowBook', function() {
        it("should not allow borrowing book if value send is less than 100", async function() {
            await lms.addBook('a', 'b', 'c', 'e', 'f', 'g');
            await lms.addMember('Michael Scofield', accounts[2], "[email protected]");
            await lms.borrowBook(1, {from: accounts[2], value: 10**12})
            await expectThrow(lms.borrowBook(1, {from: accounts[2], value: 10000})); // should throw exception
        });
});
6
  • doesn't work if ran in parity/geth. Only does with testrpc
    – rstormsf
    Aug 11, 2017 at 1:17
  • What error do you get in geth? I'm not familiar with parity so can't say about it.
    – Sanchit
    Aug 11, 2017 at 15:39
  • 1
    Also, you can add more errors which you may expect in the test case. Right now only out of gas and invalid jump are listed as errors in helpers/expectThrow.js
    – Sanchit
    Aug 11, 2017 at 15:43
  • 1
    SyntaxError: Unexpected token import github.com/trufflesuite/truffle/issues/664 Apr 4, 2018 at 18:15
  • 2
    Truffle dev here - this should work w/ geth & parity as long as you're using the latest version of truffle-contract. If it doesn't, please raise an issue!
    – Ben Burns
    Nov 6, 2018 at 20:03
12

In my opinion the cleanest possible way is the following:

it("should reject", async function () {
    try {
        await deployedInstance.myOperation1();
        assert.fail("The transaction should have thrown an error");
    }
    catch (err) {
        assert.include(err.message, "revert", "The error message should contain 'revert'");
    }
});

No need for return. Several checks can be made in the same function.

10

The other answers in this thread appear to be valid, however I believe this code is more succinct and readable.

This works with solidity 0.4.12-develop

it("should throw if the car is not blue", function() {
    return CarFactory.deployed()
        .then(function(factory) {
            return factory.createCar("red");
         })
         .then(assert.fail)
         .catch(function(error) {
                assert.include(
                    error.message,
                    'out of gas',
                    'red cars should throw an out of gas exception.'
                )
         });
});

I have noticed when using truffle+testrpc some throws cause an 'out of gas' exception and others an 'invalid opcode' exception. I have not confirmed the causes of these differing messages, yet they appear to be consistent. I advise against naively testing for both exceptions, as it is potentially useful information if the exception message changes.

0
9

Here's the pattern I currently use to test expected throws (e.g., on invalid input). Solidity implements throw by JUMPing to an invalid destination, so we catch the error and then look for the string "invalid JUMP" in the error message... I'd rather have a more robust way but haven't found anything else yet.

var EthWall = artifacts.require("./EthWall.sol");

contract('TestContract', function(accounts) {
  it("should throw an exception", function() {
    return EthWall.deployed().then(function(instance) {
      return instance.testThrow.call();
    }).then(function(returnValue) {
      assert(false, "testThrow was supposed to throw but didn't.");
    }).catch(function(error) {
      if(error.toString().indexOf("invalid JUMP") != -1) {
        console.log("We were expecting a Solidity throw (aka an invalid JUMP), we got one. Test succeeded.");
      } else {
        // if the error is something else (e.g., the assert from previous promise), then we fail the test
        assert(false, error.toString());
      }
    });
  });
});
1
  • You can shorten this code (and improve readability, IMO) by replacing your .then() block with .then(assert.fail). I have posted another answer with the full code in this question.
    – paulhauner
    Jun 11, 2017 at 12:51
2

You can use this gist I created:

var expectedExceptionPromise = function (action, gasToUse) {
  return new Promise(function (resolve, reject) {
      try {
        resolve(action());
      } catch(e) {
        reject(e);
      }
    })
    .then(function (txn) {
      // https://gist.github.com/xavierlepretre/88682e871f4ad07be4534ae560692ee6
      return web3.eth.getTransactionReceiptMined(txn);
    })
    .then(function (receipt) {
      // We are in Geth
      assert.equal(receipt.gasUsed, gasToUse, "should have used all the gas");
    })
    .catch(function (e) {
      if ((e + "").indexOf("invalid JUMP") || (e + "").indexOf("out of gas") > -1) {
        // We are in TestRPC
      } else if ((e + "").indexOf("please check your gas amount") > -1) {
        // We are in Geth for a deployment
      } else {
        throw e;
      }
    });
};
2
  • Thanks! I was able to use this snippet, and follow the examples from the gist to successfully check for a throw in a single case, however it seems to screw up execution of subsequent tests. Do you use this expectedExceptionPromise across multiple tests checking for different throws, and not see testRPC continuing to expose the invalid JUMP error?
    – thedob
    Oct 9, 2016 at 17:47
  • It does not mess up my subsequent it("", function() {}) tests, neither on TestRPC, nor on Geth. Perhaps both your tests are using a common variable. Oct 9, 2016 at 22:59
2

The other answers won't work for newer versions of Solidity (0.4.10 and above I believe).

Instead, I use a similar pattern, but with two string comparisons to catch the new error message, as well as the older one (just for legacy contracts/tests).

function assertThrows (fn, args) {
  //Asserts that `fn(args)` will throw a specific type of error.
  return new Promise(
    function(resolve, reject){
      fn.apply(this, args)
      .then(() => {
        assert(false, 'No error thrown.');
        resolve();
      },
      (error) => {
        var errstr = error.toString();
        var newErrMsg = errstr.indexOf('invalid opcode') != -1;
        var oldErrMsg = errstr.indexOf('invalid JUMP') != -1;
        if(!newErrMsg && !oldErrMsg)
          assert(false, 'Did not receive expected error message');
        resolve();
      })
  })
}

Relevant issue in Solidity GH

2

Starting from v2.0 OpenZeppelin has expectEvent helper instead of expectThrow. Here's a way to use it:

import {reverting} from 'openzeppelin-solidity/test/helpers/shouldFail';

it('your test name', async () => {
    await reverting(contract.myMethod(argument1, argument2, {from: myAccount}));
})
1

Most answers to this question that use inline try catch statements add quite a bit of boilerplate to all tests that are trying to use this method. Instead, my truffle-assertions library allows you to make assertions for any kind of Solidity throw or function failure in a very straightforward way.

The library can be installed through npm and imported at the top of the test javascript file:

npm install truffle-assertions

const truffleAssert = require('truffle-assertions');

After which it can be used inside the tests:

await truffleAssert.fails(contract.failingFunction(), truffleAssert.ErrorType.INVALID_JUMP);
0

OpenZeppelin has an expectThrow helper that's useful for this. It is located in test/helpers/expectThrow.js

module.exports = async promise => {
  try {
    await promise;
  } catch (error) {
    // TODO: Check jump destination to destinguish between a throw
    //       and an actual invalid jump.
    const invalidOpcode = error.message.search('invalid opcode') >= 0;
    // TODO: When we contract A calls contract B, and B throws, instead
    //       of an 'invalid jump', we get an 'out of gas' error. How do
    //       we distinguish this from an actual out of gas event? (The
    //       testrpc log actually show an 'invalid jump' event.)
    const outOfGas = error.message.search('out of gas') >= 0;
    assert(
      invalidOpcode || outOfGas,
      "Expected throw, got '" + error + "' instead",
    );
    return;
  }
  assert.fail('Expected throw not received');
};

example usage is in test/MintableToken.js for example:

import expectThrow from './helpers/expectThrow';
...

await expectThrow(token.mint(accounts[0], 100));
...
0

Here is another approach (inspired from above solutions).

When defining custom expect functions like these (feel free to add more), I feel the tests are then more explicit about what you expect.

// expectThrow.js

const expectThrow = (text) => async (promise) => {
   try {
     await promise;
   } catch (error) {
     assert(error.message.search(text) >= 0, "Expected throw, got '" + error + "' instead")
     return
   }
   assert.fail('Expected throw not received')
 }

 module.exports =  {
   expectOutOfGas: expectThrow('out of gas'),
   expectRevert: expectThrow('revert'),
   expectInvalidJump: expectThrow('invalid JUMP')
 }

Then in your test yo do for example:

/// test.js

const { expectRevert } from './expectThrow.js'

it('your test name', async () => {
  await expectRevert(
    // your contract call
  )
})

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.