Solidity Custom Errors: Cutting Revert Gas Costs in Production Contracts
Replace require strings with Solidity custom errors to cut revert gas costs and get structured, decodable failure data. Works from 0.8.4 onward; requires ABI publishing for explorer support.
21 Mar 2026, 05:22 UTC

The Problem: Expensive Revert Strings
Every time a Solidity contract reverts with a require(condition, "long error message"), the entire string is ABI-encoded into the revert data. On mainnet, that means paying for dozens of extra bytes of calldata on every failed transaction. For high-throughput contracts — token bridges, AMMs, lending protocols — those bytes add up to real ether.
Since Solidity 0.8.4, custom errors let you replace those strings with a compact four-byte selector plus ABI-encoded arguments. The error definition lives once in the contract bytecode; each revert only emits the selector and the arguments you actually need.
How Custom Errors Work
Declare an error like an event, then trigger it with revert:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
contract Vault {
error InsufficientBalance(uint256 requested, uint256 available);
error Unauthorized(address caller);
mapping(address => uint256) public balances;
function withdraw(uint256 amount) external {
if (balances[msg.sender] < amount) {
revert InsufficientBalance(amount, balances[msg.sender]);
}
if (msg.sender != owner()) {
revert Unauthorized(msg.sender);
}
balances[msg.sender] -= amount;
}
}The ABI now contains entries of type error with name, inputs, and anonymous: false. Block explorers and off-chain tooling (Foundry, Hardhat, ethers.js) decode the revert payload automatically when they have the ABI.
Worked Example: Gas Comparison
Below is a minimal Foundry test that measures the difference. Run it with forge test --gas-report on a compiler ≥ 0.8.4.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
import "forge-std/Test.sol";
contract GasCompareTest is Test {
error CustomErr(uint256 code);
function testRequireString() external {
vm.expectRevert("insufficient balance for withdrawal");
require(false, "insufficient balance for withdrawal");
}
function testCustomError() external {
vm.expectRevert(CustomErr(42));
revert CustomErr(42);
}
}Typical results (Foundry gas snapshot, optimizer enabled):
| Scenario | Gas Used |
|---|---|
require(false, "long string") | ~23,500 |
revert CustomErr(42) | ~22,800 |
The savings come from avoiding the full string in calldata. The selector (4 bytes) plus a single uint256 argument (32 bytes) is far smaller than the UTF-8 bytes of a descriptive message.
Trade-offs and Limitations
- ABI dependency: Without the contract ABI, explorers show only the four-byte selector (e.g.,
0x1a2b3c4d). Teams must publish ABIs or maintain a shared error registry. - No built-in human text: Custom errors carry no default message. Documentation must map each error name and its parameters to a readable explanation.
- Version floor: Contracts targeting older EVM chains (pre-0.8.4) cannot use this feature.
- External call reverts: An uncaught revert from a low-level call still bubbles up and rolls back the whole transaction unless wrapped in
try/catch.
Actionable Checklist
- Confirm
solc --versionreports 0.8.4 or newer. - Replace every
require(cond, "message")with a namederrorandrevert ErrorName(args). - Add the generated ABI to your explorer verification (Etherscan, Sourcify) so revert data decodes automatically.
- Write a unit test that asserts the exact error name and decoded arguments using
vm.expectRevertor ethers.jserrormatchers. - Run a gas snapshot before and after the migration to quantify savings for your specific contract.
Custom errors are a low-effort, high-impact upgrade for any production Solidity codebase. The gas savings compound across every failed user transaction, and the structured revert data makes debugging dramatically easier once the ABI is available.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.