Test Solidity Smart Contract Errors with Hardhat

A reverted transaction should tell you which rule failed and let your test prove that result. This example uses Solidity custom errors and Hardhat to check an unauthorized withdrawal and a deposit below the contract minimum.

Create a contract with named failure states

Custom errors give each failure a name and can carry values that explain it, while Solidity documents that a state-reverting exception undoes changes in the active call and its sub-calls.

The Vault contract has one external rule for each test. Only the owner may withdraw, and every deposit must meet MINIMUM_DEPOSIT.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.34;

contract Vault {
    error Unauthorized(address caller);
    error AmountTooSmall(uint256 provided, uint256 minimum);

    address public immutable owner;
    uint256 public constant MINIMUM_DEPOSIT = 1 ether;

    constructor() { owner = msg.sender; }

    function deposit() external payable {
        if (msg.value < MINIMUM_DEPOSIT) {
            revert AmountTooSmall(msg.value, MINIMUM_DEPOSIT);
        }
    }

    function withdraw() external {
        if (msg.sender != owner) revert Unauthorized(msg.sender);
    }
}

The constructor stores the deployer as owner, deposit checks the sent value before it changes storage, and withdraw rejects a caller whose address differs from owner.

Set up the Hardhat test

Install Hardhat with its Ethers and Chai matcher plugins, then place the contract in contracts/Vault.sol and the test in test/Vault.js. The matcher receives the contract instance and the custom-error name, which prevents a broad “any revert” assertion from hiding the wrong failure.

import { expect } from "chai";
import { network } from "hardhat";

const { ethers } = await network.getOrCreate();

describe("Vault", function () {
  async function deployVault() {
    const [owner, other] = await ethers.getSigners();
    const vault = await ethers.deployContract("Vault");
    return { vault, owner, other };
  }

  it("rejects a withdrawal from a non-owner", async function () {
    const { vault, other } = await deployVault();
    await expect(vault.connect(other).withdraw())
      .to.be.revertedWithCustomError(vault, "Unauthorized")
      .withArgs(other.address);
  });

  it("reports the supplied and required deposit amounts", async function () {
    const { vault } = await deployVault();
    await expect(vault.deposit({ value: ethers.parseEther("0.5") }))
      .to.be.revertedWithCustomError(vault, "AmountTooSmall")
      .withArgs(ethers.parseEther("0.5"), ethers.parseEther("1"));
  });
});

Test the unauthorized withdrawal

deployVault returns a second signer named other. Calling withdraw through vault.connect(other) changes msg.sender, so the contract reaches Unauthorized and returns the rejected address as its argument.

The withArgs check binds the failure to the caller you supplied, so the assertion fails if the contract reverts for another reason.

Test the amount rule

The second test sends 0.5 ether to deposit while the contract requires 1 ether. It expects AmountTooSmall and checks both values, so the contract must report the supplied amount and its minimum in the same order as the error declaration.

I ran npx hardhat test with the contract and test shown here. Hardhat compiled Solidity 0.8.34 and reported both assertions as passing.

Hardhat terminal output showing two passing Solidity custom-error tests
Hardhat reports both custom-error assertions as passing.

Choose require, revert, or assert

Use require when a condition can be written directly beside the check, and use revert when a branch needs a named custom error or arguments, as it does in Vault.

Use assert for an invariant that indicates a defect in contract logic. Solidity treats failed assertions as Panic errors, so they do not describe invalid user input.

  • Use a custom error when the caller needs a precise failure name or values.
  • Test the exact error and its arguments for every rule that rejects an external call.
  • Keep success tests beside failure tests so a rule cannot become impossible to satisfy.

Run the test and add the next rule

Run npx hardhat test from the project directory to check one boundary rather than the contract’s security, access design, or deployment configuration.

Add the error assertion that belongs to the next externally callable rule before you deploy. The Solidity error-handling reference and the Hardhat matcher documentation cover the underlying behavior and matcher options.

Aneesha S
Aneesha S

Aneesha S writes practical guides to MongoDB, Mongoose, and Node.js. Her articles cover document queries and updates, file operations, and HTTP requests.

Articles: 169