Optimizing Smart Contract Reverts with Solidity Custom Errors
Learn how to replace expensive string-based reverts with Solidity custom errors to reduce gas costs, improve ABI decoding, and implement typed error handling.
05 Jul 2025, 20:28 UTC

The Problem: Gas Waste in String Reverts
Traditional Solidity require("Error Message") statements are expensive. Every time a contract reverts with a string, the EVM must store that string in the contract bytecode and expand memory to return it to the caller. For complex systems with dozens of failure modes, this increases deployment costs and runtime gas overhead for every failed transaction.
The solution is Custom Errors (introduced in Solidity 0.8.4). Instead of returning a long string, custom errors return a 4-byte selector followed by ABI-encoded parameters. This shifts the burden of "human-readable" messages from the blockchain to the frontend or client-side toolchain.
Smallest Suitable Design
The most efficient implementation avoids generic "ErrorCode" enums. Instead, define a unique error type for every distinct failure state. This allows for typed error handling and minimizes the data passed during a revert.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract Vault {
// Define specific error types
error InsufficientBalance(uint256 available, uint256 required);
error UnauthorizedCaller(address caller);
error TransferFailed();
mapping(address => uint256) public balances;
function withdraw(uint256 amount) external {
if (msg.sender != owner()) revert UnauthorizedCaller(msg.sender);
if (balances[msg.sender] < amount) {
revert InsufficientBalance(balances[msg.sender], amount);
}
// Logic for transfer...
if (!doTransfer(msg.sender, amount)) revert TransferFailed();
}
function owner() internal view returns (address) { return address(0x123); }
function doTransfer(address to, uint256 amt) internal pure returns (bool) { return true; }
}
Comparison: String vs. Custom Error
| Feature | require("String") |
revert CustomError() |
|---|---|---|
| Deployment Gas | Higher (Strings stored in bytecode) | Lower (Only selectors stored) |
| Runtime Gas | Higher (Memory expansion) | Lower (Compact ABI encoding) |
| Data Payload | Static text | Dynamic parameters (uint, address, etc.) |
| Client Decoding | Direct string read | Requires ABI/Selector mapping |
Trust and Data Boundaries
Custom errors expose structured data to the public. While this is useful for debugging, it creates a data boundary risk: never include sensitive internal state or private nonces in error parameters. Any data passed into a revert is visible to anyone monitoring the mempool or block explorer.
From a trust perspective, the caller can rely on the error selector to trigger specific UI flows (e.g., triggering a "Top Up Balance" modal when InsufficientBalance is caught), but the contract should not assume the caller will handle these errors gracefully.
Operational Checks and Verification
To verify that custom errors are functioning and providing the expected gas savings, use the following workflow in a Foundry environment.
1. Gas Analysis
Run a gas report to compare the cost of a string-based revert against a custom error revert:
# Run tests with gas reporting enabled
forge test --gas-report
2. Manual Decoding
If a transaction reverts and you only have the raw data, use cast to decode the error selector. This is critical for debugging unverified contracts.
# Decode a revert payload using the error signature
# Run on local terminal with cast installed
cast abi-decode "error InsufficientBalance(uint256,address)" <REVERT_DATA_HEX>
3. Programmatic Catching
In Solidity 0.8.14+, you can use try/catch blocks to handle specific custom errors in a parent contract:
try vault.withdraw(amount) {
// Success
} catch InsufficientBalance(uint256 available, uint256 required) {
// Handle specific balance error
} catch {
// Handle all other errors
}
Failure Modes and Design Shifts
Proxy Upgradeability
When using proxy patterns (EIP-1967), changing an error signature in the implementation contract will break off-chain decoding for the frontend. If you rename InsufficientBalance to BalanceTooLow, the 4-byte selector changes, and the frontend will see an "Unknown Error" until the ABI is updated. To mitigate this, maintain a stable set of error definitions in a shared library used by all implementation versions.
Tooling Limitations
If your project must support legacy toolchains or extremely old block explorers that do not recognize custom error selectors, you may be forced to use string reverts. In these cases, wrap your reverts in a helper library to maintain consistency across the codebase.
Rollback Procedure
Since custom errors are a language feature and not a state-changing operation, there is no "rollback" for the logic itself. However, if a deployment is found to have incorrect error signatures that break critical frontend integrations, the implementation contract must be redeployed (or the proxy pointed to a new implementation) with the corrected error signatures.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.