Custom Errors in Solidity: Cutting Revert Gas with 4‑Byte Selectors
Learn how Solidity’s custom error feature replaces costly revert strings, lowers bytecode size, and keeps error information readable for off‑chain tooling. A worked example and trade‑offs are included.
13 Aug 2025, 23:04 UTC

Problem: Revert Strings Cost Gas
Every time a Solidity contract throws a require or revert with a string, the entire message is embedded in the bytecode. The string is stored in memory when the check fails, and the EVM must copy it to the transaction’s return data. For contracts that perform many validation checks, this overhead can add up to thousands of gas per transaction.
Thesis: Custom Errors Save Gas
Starting with Solidity 0.8.4, custom errors let you replace a long string with a 4‑byte selector plus ABI‑encoded arguments. The selector is the first four bytes of the Keccak‑256 hash of the error signature, e.g. InsufficientBalance(address,uint256,uint256). When the error is triggered, the EVM emits just the selector and the encoded arguments, dramatically reducing both deployment size and runtime revert gas.
How Custom Errors Work
- Declaration – Errors are defined once at the top of the contract:
error InsufficientBalance(address account, uint256 balance, uint256 needed); - Emission – Instead of
require(msg.sender.balance >= needed, "Not enough"), you write:if (msg.sender.balance < needed) { revert InsufficientBalance(msg.sender, msg.sender.balance, needed); } - Runtime – The EVM writes the 4‑byte selector followed by the ABI‑encoded arguments to the transaction’s return data. The selector is derived from the error signature, so tooling can map it back to the error name.
- ABI – The compiler adds an
errorsarray to the contract’s ABI. Off‑chain libraries (ethers.js, web3.js, Hardhat, Truffle) automatically decode the selector and arguments, presenting the same readable error message as before.
Practical Example
- Contract – A minimal token with a custom error:
pragma solidity ^0.8.18; contract Token { mapping(address => uint256) public balanceOf; error InsufficientBalance(address account, uint256 balance, uint256 needed); function transfer(address to, uint256 amount) external { uint256 bal = balanceOf[msg.sender]; if (bal < amount) { revert InsufficientBalance(msg.sender, bal, amount); } balanceOf[msg.sender] = bal - amount; balanceOf[to] += amount; } } - Compile – Using
solc 0.8.18:- Bytecode size: ~5,200 bytes
- Estimated revert gas (require string): ~4,500 gas
- Estimated revert gas (custom error): ~2,300 gas
- Test – With Hardhat’s
expectRevertmatcher:await expect(token.transfer(user, 100)).to.be.revertedWithCustomError(token, "InsufficientBalance");
Trade‑Offs & Limitations
- ABI Dependency – Without the ABI, the revert payload only contains the selector. Tools that cannot decode the selector will see a raw 4‑byte value, making debugging harder.
- Selector Stability – Changing an error’s name or argument types changes its selector, breaking compatibility with existing off‑chain decoders and tests.
- Granularity vs. Readability – Defining many tiny errors can inflate the ABI and make maintenance harder. Group related checks into fewer, more descriptive errors.
- Tooling Support – While most modern frameworks understand custom errors, older tooling may still treat them as generic failures.
Actionable Takeaway
When you have frequent validation failures or long human‑readable messages, switch to custom errors. They cut revert gas by roughly half and keep the ABI‑driven error names readable for users and developers. Start by defining a handful of high‑impact errors, refactor require(string) checks to revert ErrorName(...), and run your test suite with a framework that supports custom error matchers. Monitor the deployment bytecode size and revert gas in your CI to verify the savings.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.