Custom Errors in Solidity: Reduce Gas, Keep Tooling Intact
Learn how Solidity custom errors replace expensive string reverts, reduce gas, and stay decodable by tooling. Includes a practical example, verification steps, and trade‑off discussion.
13 Sept 2026, 11:00 UTC

The Problem: Expensive Revert Messages
When a contract needs to signal an error, the most common pattern is require(condition, "human‑readable message"). The string is stored in the revert data, which means every byte of the message is paid for both in bytecode size and at runtime. In contracts that revert often—such as token transfers with many checks—those costs can add up.
Thesis: Structured Errors Cut Gas While Remaining Decodable
Starting with Solidity 0.8.4, the language introduced the error keyword. A custom error is a named type that can carry typed arguments. At runtime the EVM only needs to store the 4‑byte selector (the first four bytes of the keccak256 hash of the error signature) plus the ABI‑encoded arguments. This is usually far smaller than an arbitrary‑length string, leading to measurable gas savings while still allowing tooling to decode the error data via the contract’s ABI.
How Custom Errors Work
- Define an error:
error Name(type1 arg1, type2 arg2, …); - Trigger it:
revert Name(arg1, arg2); - Include the error definition in the ABI so external tools can decode the selector and arguments.
Worked Example: A Simple Token with Custom Errors
The following contract demonstrates two custom errors—InsufficientBalance and ZeroAddress—used in a transfer function. The errors carry only the essential data needed for a UI to display a helpful message.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract SimpleToken {
mapping(address => uint256) public balance;
uint256 public totalSupply;
error InsufficientBalance(address user, uint256 needed, uint256 available);
error ZeroAddress();
constructor(uint256 initial) {
totalSupply = initial;
balance[msg.sender] = initial;
}
function transfer(address to, uint256 amount) public returns (bool) {
if (to == address(0)) {
revert ZeroAddress();
}
uint256 senderBal = balance[msg.sender];
if (senderBal < amount) {
revert InsufficientBalance(msg.sender, amount, senderBal);
}
balance[msg.sender] = senderBal - amount;
balance[to] += amount;
return true;
}
}
Verification Steps
- Compile with
solc --version 0.8.20 SimpleToken.sol --abi --bin -o build. The generated ABI will contain entries like{ "type": "error", "name": "InsufficientBalance", "inputs": [...] }. - Deploy to a local Hardhat node (
npx hardhat node) and attach with ethers.js:const token = await ethers.getContractAt("SimpleToken", address); try { await token.connect(signerWithLowBalance).transfer(recipient, ethers.utils.parseEther("100")); } catch (err) { if (err.errorName) { console.log("Error:", err.errorName, "args:", err.errorArgs); } else { throw err; } } - Measure gas usage with
hardhat gasReporteror Remix’s gas debugger. Compare the cost of a custom‑error revert with a string‑basedrequirerevert to observe the reduction.
Trade‑offs & Limitations
- Custom errors add a small amount of bytecode to store the selector mapping. If an error carries many large arguments (e.g., arrays or structs), the encoded payload can grow and offset the savings.
- They require Solidity ≥ 0.8.4; older compilers reject the syntax. Contracts that need to remain compatible with older compiler versions or certain L2 environments that default to older Solidity releases must either avoid custom errors or maintain separate builds.
- While tooling such as block explorers and ethers.js can decode custom errors when the ABI includes the definition, some older tools may not recognize the
type: "error"field. In practice, the majority of modern tooling supports it.
Actionable Next Steps
- Identify the most frequent
requirestatements in your contract. Replace them with custom errors that carry only the minimal data needed for a user‑friendly message. - Add the error definitions to the ABI and commit them alongside the contract source. This ensures front‑ends and explorers can decode the revert data automatically.
- Run a gas comparison on a representative test case. If you see a noticeable reduction (often a few hundred gas units for simple errors), the switch is worthwhile.
- Keep the error definitions version‑controlled so that any changes are visible to developers and tooling consumers.
Conclusion
Custom errors provide a clean, gas‑efficient way to signal failures in Solidity contracts while preserving the ability for tooling and front‑ends to present meaningful messages. By adopting them thoughtfully—limiting payload size and ensuring compiler compatibility—you can reduce execution costs without sacrificing clarity.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.