Using Solidity Custom Errors for Gas‑Efficient and Type‑Safe Reverts
Learn how Solidity custom errors (since 0.8.4) cut deployment size, provide typed revert data, and simplify testing—complete with a token‑gated example, trade‑offs, and verification steps.
09 Mar 2026, 11:19 UTC

Problem: String‑based reverts are noisy and costly
When a Solidity function fails, developers often write require(condition, "not authorized") or revert("not authorized"). The reason string lives in the contract’s bytecode, increasing deployment size, and off‑chain tools must match raw text to interpret the failure. This approach is brittle: a typo in the test string or a change in the message breaks assertions, and explorers show only a hex string unless the source is verified.
Thesis: Custom errors give cheaper deployment, typed data, and easier testing
Introduced in Solidity 0.8.4, a custom error is declared like a function, e.g. error Unauthorized(address caller);. When reverted with revert Unauthorized(msg.sender);, the error is ABI‑encoded as a four‑byte selector followed by the encoded arguments. This encoding is stored only once in the contract’s metadata, so deployment bytecode is usually smaller than storing a long revert string. Because the error carries typed parameters, libraries such as ethers.js, viem, Foundry, or Hardhat can decode the payload and expose it to tests and front‑ends.
Worked example: A token‑gated withdrawal
Consider a simple ERC‑20‑like contract where only holders with sufficient balance may withdraw. Using a custom error avoids a string comparison in tests.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
// Declared at file level – can be reused by any contract or interface
error InsufficientBalance(address user, uint256 balance, uint256 needed);
contract TokenVault {
mapping(address => uint256) public balanceOf;
function deposit() external payable {
balanceOf[msg.sender] += msg.value;
}
function withdraw(uint256 amount) external {
uint256 bal = balanceOf[msg.sender];
if (bal < amount) {
// Revert with typed data
revert InsufficientBalance(msg.sender, bal, amount);
}
balanceOf[msg.sender] = bal - amount;
payable(msg.sender).transfer(amount);
}
}
In a Foundry test we can assert the exact error and inspect its arguments:
function testWithdrawFails() public {
vm.startPrank(address(1));
vault.deposit{value: 1 ether}();
vm.stopPrank();
// Expect the custom error with the three arguments
vm.expectRevert(abi.encodeWithSelector(
TokenVault.InsufficientBalance.selector,
address(1),
1 ether,
2 ether // requested amount > balance
));
vault.withdraw(2 ether);
}
The test passes only if the revert matches the selector and the three values, eliminating the risk of a mismatched string.
Trade‑offs and limitations
- Deployment savings: The error selector (4 bytes) plus argument types are stored once in the contract’s metadata. A long revert string (e.g., 64‑byte message) would be duplicated in the bytecode for each
requireorrevertstatement. - Runtime cost: When a revert occurs, the EVM still needs to copy the error data to return data. For very short strings the difference is negligible; for long strings the custom error can be slightly cheaper because the selector is fixed‑size.
- Readability in raw logs: Block explorers that do not have the contract ABI will show the error as a hex string (the selector) rather than a readable message. Teams must ensure that tooling (e.g., Etherscan verification) includes the ABI so the error is decoded.
- Version sensitivity: The ABI encoding of errors is stable, but optimizer behavior can affect the exact bytecode size. Always verify against the compiler version you plan to deploy.
Verification steps
- Bytecode comparison: Compile two minimal contracts—one using
require(..., "msg")and one using a custom error—with the same Solidity version (e.g., 0.8.20). Compare thedeployedBytecodelength reported bysolc --combined-json abi,bin-runtime. The custom error version should be equal or smaller. - Test decoding: Write a test (Foundry, Hardhat, or Truffle) that expects the revert and uses
abi.encodeWithSelectorto match the error selector and arguments. Confirm the test passes only when the correct error is reverted. - Off‑chain decoding: Using ethers.js, call a failing transaction and decode the return data with
ethers.utils.defaultAbiCoder.decodeor the contract’s interface. Verify that the decoded values match the arguments passed torevert.
Actionable closing
If you are starting a new Solidity project or auditing an existing one, replace verbose require strings with custom errors for any revert that carries meaningful data. Declare the errors at the file level, document them in your interface, and add a test that asserts the encoded selector and arguments. This yields smaller deployment artifacts, stronger type safety, and more reliable automated checks—without sacrificing readability when the ABI is available.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.