Replacing Revert Strings with Custom Errors in Solidity: A Practical Migration Guide
Replace revert strings with Solidity custom errors (0.8.4+) to shrink bytecode and cut revert gas. Covers the conversion pattern, test updates, gas verification, and tooling caveats.
30 Sept 2026, 22:06 UTC

Why revert strings are worth removing
Every require(condition, "Insufficient balance") in your contract stores that string in the deployed bytecode and pays to ABI-encode it on every revert. A contract with a dozen distinct messages carries a dozen string literals forever. Custom errors, available since Solidity 0.8.4, replace those strings with a 4-byte selector plus ABI-encoded parameters. The result is smaller deployment bytecode, cheaper reverts (typically tens to a couple hundred gas per revert path depending on string length), and structured data your tests and frontends can actually use.
This guide walks through converting an existing contract, verifying the savings, and handling the tooling gaps you may hit.
Prerequisites
- Solidity compiler 0.8.4 or later. Check with
solc --versionor yourpragma solidityline. Custom errors do not exist in earlier versions. - A test suite that covers your revert paths (Hardhat or Foundry). You will be changing revert behavior, so untested paths are a risk.
- A gas reporting setup:
forge test --gas-report/forge snapshotfor Foundry, orhardhat-gas-reporterfor Hardhat.
The conversion pattern
Declare errors at file or contract scope, outside functions. Include parameters that make debugging actionable:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
error InsufficientBalance(uint256 available, uint256 required);
error Unauthorized(address caller);
error ZeroAmount();
contract Vault {
mapping(address => uint256) public balances;
function withdraw(uint256 amount) external {
if (amount == 0) revert ZeroAmount();
uint256 bal = balances[msg.sender];
if (bal < amount) revert InsufficientBalance(bal, amount);
balances[msg.sender] = bal - amount;
// ... transfer logic
}
}Two mechanical replacements cover most code:
revert("Insufficient balance");becomesrevert InsufficientBalance(bal, amount);require(bal >= amount, "Insufficient balance");becomesif (bal < amount) revert InsufficientBalance(bal, amount);
Note the inverted condition in the second form — this is the most common transcription mistake during migration. require states what must be true; the if states the failure case.
Prefer fixed-size parameter types (uint256, address, bytes32). Dynamic types like string or uint256[] in error parameters increase encoding cost and erode the savings you are doing this for.
Updating tests
Tests that assert on revert strings must change. With Hardhat and ethers v6 (via hardhat-chai-matchers):
await expect(vault.withdraw(1000))
.to.be.revertedWithCustomError(vault, "InsufficientBalance")
.withArgs(0, 1000);With Foundry, use the selector:
vm.expectRevert(
abi.encodeWithSelector(InsufficientBalance.selector, 0, 1000)
);
vault.withdraw(1000);Asserting on arguments, not just the error name, is where custom errors pay off: your test now proves the contract reported the correct available balance, which a string comparison never could.
Verifying the savings
Do not assume the win — measure it. Before converting, capture a baseline:
- Foundry: run
forge snapshotin your project root to write a.gas-snapshotfile, and note the deployed contract size fromforge build --sizes. - Hardhat: run
npx hardhat testwith the gas reporter enabled and save the output.
After conversion, rerun both. Compare the revert-path rows in the gas report and the deployed bytecode size. Expect the biggest bytecode reduction in contracts with many long, unique revert strings; a contract with two short messages will show modest gains. If a path shows no improvement, check whether you left a string behind or introduced a dynamic-type error parameter.
To confirm decoding works end to end, deploy to a local node (Anvil or Hardhat Network), trigger a revert, and inspect the trace — cast run <tx-hash> against a fork, or Tenderly for a shared trace. You should see the error name and decoded arguments, not raw hex.
Tooling and compatibility caveats
- Selector stability: the 4-byte selector is derived from the error signature (name plus parameter types). Changing
InsufficientBalance(uint256,uint256)to add a parameter changes the selector and silently breaks every off-chain decoder and test using the old signature. Treat error signatures as part of your public ABI. - Older wallets and explorers: some older MetaMask versions and unverified explorer pages display raw hex instead of the decoded error. Verified contracts on current Etherscan decode custom errors, but verify on your target chain's explorer before relying on it for user-facing messages.
- Interfaces and libraries: they can declare custom errors (a common pattern — declare shared errors in an interface so multiple contracts revert with identical selectors), but the reverting contract must actually use them.
- Static analysis: run
slither . --detect unused-errorafter migration to catch errors you declared but never referenced.
Recovery options if something breaks
Because this change alters revert data, the main risk is off-chain: a frontend or indexer that string-matched "Insufficient balance" now gets a selector it does not recognize.
- If a legacy frontend cannot decode custom errors yet, ship a selector-to-message mapping in the frontend rather than reverting the contract change — the mapping is a one-time cost and keeps the gas savings.
- If your contract calls external contracts that may revert with custom errors, use try/catch and handle the low-level
bytesin the catch block; do not assumeError(string). - If a pre-deployment audit requires the old behavior, the change is fully revertible in source — keep the migration in its own commit so you can roll back cleanly before deployment. After deployment, revert data is part of the observable contract behavior, so treat mainnet deployment as the point of no easy return.
Done carefully, this is a mechanical, testable refactor: define errors, invert the requires, update assertions, and let the gas report confirm the outcome.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.