Skip to main content
Create your own

Testing Reverts with `revertedWith`

Welcome back to our series on smart contract testing. In the last lesson, you learned how to write tests for the "happy path"—verifying that your contract's functions behave as expected when used correctly. This is like an ETL developer confirming that a pipeline successfully processes and loads valid data. However, a truly robust system must also handle invalid inputs gracefully.

This lesson focuses on testing the "unhappy path." We'll explore how to write tests that ensure your smart contract correctly rejects invalid actions. Just as a data validation process must have rules to reject malformed records, a smart contract needs require statements to enforce its internal logic. Your job as a developer is to prove that these safeguards work.

By the end of this lesson, you will be able to test for expected reverts on failed conditions, a critical skill for building secure and predictable smart contracts.

The output of a comprehensive test suite. Notice the sections for "Success" and "Failure." The checkmarks under "Failure" indicate tests that successfully confirmed the contract rejected an invalid action, which is precisely our goal for today.

1. Why Test for Failure?

In smart contract development, security is paramount. A function that can be called with incorrect arguments or by an unauthorized user can lead to bugs, exploits, and financial loss. You write require() statements in Solidity to create guards against such scenarios. For example:

  • require(amount > 0, "Amount must be positive");
  • require(msg.sender == owner, "Only owner can call this");

When a require statement fails, the transaction is reverted. This means all state changes are undone, and the gas spent up to that point is consumed. Testing for reverts is your way of guaranteeing these security checks are implemented correctly and are not bypassed.

2. Asserting Reverts with revertedWith

To test for reverts, Hardhat provides a powerful matcher through the hardhat-ethers-chai-matchers plugin (which is already part of the hardhat-toolbox you installed). The primary tool you'll use is .to.be.revertedWith().

The syntax is straightforward:

await expect(contract.someFunction(...)).to.be.revertedWith("Exact error message from require");

There are two key things to note here:

  1. The expect() function is wrapped around the entire asynchronous function call (contract.someFunction(...)), which returns a Promise for a transaction. You are asserting on the transaction promise itself, not its resolved value.
  2. The string provided to revertedWith() must be an exact match for the reason string in your Solidity require statement.

Let's see this in action with a complete example. The following video demonstrates testing a contract that has several conditions enforced by require.

Hardhat Testing Tutorial | Solidity Smart Contract Testing Developer | Hardhat Testing Course

This video walks through writing a contract with several require statements and then builds a test suite to verify that each one causes a revert under the correct conditions.

First, familiarize yourself with the example contract by watching from the contract code. The contract, myTest, has require statements in both its constructor (to ensure unlockTime is in the future) and its withdraw function (to check unlockTime and the caller's identity). Next, watch how to test the constructor's require statement from testing the constructor revert. The test attempts to deploy the contract with an invalid timestamp and uses expect(...).to.be.revertedWith("your unlock time should be in future") to assert that the deployment itself fails as expected. Now, see how a function precondition is tested. Watch from testing a function revert. This test calls the withdraw function before the unlock time has passed and asserts that it reverts with the correct error message. Notice at the failed test how changing the case of the error string ("wait" vs. "Wait") causes the test to fail, highlighting the need for an exact match. Finally, observe how to test access control. Watch from testing the owner check. This is a crucial pattern. The test uses contract.connect(otherAccount).withdraw() to simulate a call from a different user and asserts that the transaction is reverted because the caller is not the owner.

The .connect(signer) method is your primary tool for testing multi-user interactions and is essential for verifying access control logic. You get access to different accounts (signers) using await ethers.getSigners().

3. A Tour of Revert Matchers

While .revertedWith() is the most common matcher you'll use, the hardhat-ethers-chai-matchers library provides a few others for different scenarios. It's useful to be aware of them.

hardhat-ethers-chai-matchers

This official documentation details the various matchers available for testing reverted transactions.

Please read the section on revert matchers. Pay attention to the purpose of each one: .revert(): Use this when you only care that the transaction reverted, regardless of the reason. .revertedWith(reason): The one we're focusing on. Asserts a revert with a specific string. .revertedWithCustomError(contract, errorName): For testing custom errors, an advanced feature we will cover in a later module. .revertedWithPanic(code): For asserting low-level panic codes (e.g., from an array out-of-bounds access).

4. An Important Practical Tip: No Chaining

A common pitfall is trying to chain multiple asynchronous matchers together. For example, you might want to check that a function both changes a balance and emits an event. The hardhat-ethers-chai-matchers library does not support this.

hardhat-ethers-chai-matchers

This section of the documentation explains a key limitation and how to work around it.

Please read the section on known limitations. The key takeaway is that you cannot chain matchers like revert or changeEtherBalance. Instead, you must store the transaction promise in a variable and write separate expect assertions for it. Incorrect (Chaining):

Incorrect (Chaining):

await expect(contract.f(...))
  .to.revert()
  .and.to.emit("SomeEvent"); // This will not work

Correct (Separate Assertions):

const tx = contract.f(...);

await expect(tx).to.revert();
await expect(tx).to.emit(contract, "SomeEvent");

This pattern is crucial for writing clean and correct tests for complex transactions.

5. Your Turn: Add a Revert Test to Box

Now, let's apply what you've learned to the Box contract from our previous lessons.

  1. Modify the Box.sol contract. Add a require statement to the store function to prevent a user from storing the value zero.

    // contracts/Box.sol
    // SPDX-License-Identifier: MIT
    pragma solidity ^0.8.24;
    
    contract Box {
        uint256 private value;
    
        function store(uint256 newValue) public {
            // Add this line:
            require(newValue != 0, "Value cannot be zero");
            value = newValue;
        }
    
        function retrieve() public view returns (uint256) {
            return value;
        }
    }
    

    Don't forget to re-compile your contracts after this change: npx hardhat compile.

  2. Add a new test case to Box.test.ts. Create a new it block that specifically tests this new failure condition.

    // test/Box.test.ts
    
    it("Should revert when trying to store a value of 0", async function () {
      // Arrange: Load the contract from the fixture
      const { box } = await loadFixture(deployBoxFixture);
    
      // Act & Assert: Expect the call to store(0) to be reverted with the specific error message
      await expect(box.store(0)).to.be.revertedWith("Value cannot be zero");
    });
    

    Place this new test inside your describe("Box Contract", ...) block, alongside the existing tests.

  1. Run your tests. Execute npx hardhat test in your terminal. You should see all your tests pass, including the new one that confirms your require statement is working correctly.

Conclusion

You have now learned how to test for both success and failure, a cornerstone of defensive smart contract development. By verifying that your contract correctly reverts invalid transactions, you build confidence in its security and reliability.

Here are the main takeaways from this lesson:

  • Testing for failures (the "unhappy path") is as important as testing for successful outcomes.
  • The expect(tx).to.be.revertedWith("reason") pattern is used to assert that a transaction fails with a specific error message.
  • The error string in your test must exactly match the reason string in your Solidity require statement.
  • You can simulate calls from different users with contract.connect(signer) to test access control logic.
  • Asynchronous matchers cannot be chained; you must write separate assertions for the same transaction promise.

In the next lesson, we will complete our tour of the essential testing assertions by learning how to test for event emissions, which are Solidity's way of logging important activities on the blockchain.

Can't find a good explanation? Sign up and we'll make it for you

Sign up