Solidity Custom Errors vs Require Strings: A Practical Guide
Custom errors in Solidity 0.8.4+ save deployment gas and carry typed revert data. Learn when to use them and how to decode them off-chain.
16 Dec 2025, 10:57 UTC

Why require strings fall short
When a transaction reverts, the caller needs to know why. A common Solidity pattern is:
require(msg.sender == owner, "Not owner");
The string "Not owner" is compiled into the contract bytecode. Every unique string adds deployment size, and the revert data reaches off-chain tools as a plain string rather than structured values. If you have twenty access checks, you pay for twenty string literals.
What custom errors do instead
Introduced in Solidity 0.8.4, custom errors are declared like this:
error NotOwner(address caller, address owner);
They are triggered with revert:
revert NotOwner(msg.sender, owner);
The compiler stores a four-byte selector derived from the error name and parameter types, then ABI-encodes the arguments in the revert data. Wallets and block explorers that have the contract ABI can decode those arguments and display structured context instead of a generic message.
Worked example: a vault withdrawal
This contract guards a withdrawal function with two custom errors. It assumes Solidity 0.8.24 or a compatible 0.8.x compiler.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
contract Vault {
error Unauthorized(address caller, address owner);
error InsufficientFunds(uint256 balance, uint256 requested);
error TransferFailed();
address public immutable owner;
constructor() {
owner = msg.sender;
}
function withdraw(uint256 amount) external {
if (msg.sender != owner) {
revert Unauthorized(msg.sender, owner);
}
if (amount > address(this).balance) {
revert InsufficientFunds(address(this).balance, amount);
}
(bool sent,) = payable(msg.sender).call{value: amount}("");
if (!sent) {
revert TransferFailed();
}
}
}
Each revert carries typed arguments. Unauthorized tells off-chain tools which address tried the call and who owns the contract. InsufficientFunds sends both the current balance and the requested amount, so a frontend can show the exact shortfall without parsing a string.
Gas, deployment size, and inheritance
Custom errors generally cost less gas than equivalent require(condition, "string") statements because the selector is stored once and the revert data is ABI-encoded rather than embedded as a literal string. In validation-heavy contracts, replacing many unique require strings with custom errors can reduce deployment bytecode size.
Errors also support inheritance. An error declared in a base contract is available in derived contracts, which helps teams keep revert patterns consistent across access-control or token contracts.
Compile and inspect the ABI
Run this in your project root. It requires the solc binary in your PATH and read/write access to the project directory.
solc --version
solc --optimize --abi --bin -o artifacts/ contracts/Vault.sol
Expected check: open artifacts/Vault.abi.json and confirm it contains entries where "type" is "error". If your installed solc is older than 0.8.4, it will reject the error keyword and fail compilation.
Decoding reverts off-chain
A frontend or block explorer needs the contract ABI to turn the raw revert data into something readable. Without it, users see a hex string or an undecoded revert. With viem or ethers, you can decode the error selector and arguments from the transaction receipt or caught exception.
Conceptually, the decode flow is:
- Catch the failed transaction or read its receipt.
- Extract the revert data, which begins with the four-byte error selector.
- Match the selector against the contract ABI.
- ABI-decode the remaining bytes using the declared parameter types.
Limitations and practical verification
- Compiler version: custom errors require Solidity 0.8.4 or newer.
- Gas still consumed: a revert uses all forwarded gas unless the caller wraps the external call in
try/catch. - ABI dependency: off-chain decoding fails without the contract ABI.
- Selector design: each error signature is derived from its name and parameter types, so avoid overloading errors in ways that make selectors hard to distinguish.
To verify behavior locally, deploy the contract to Anvil or Hardhat, call withdraw from a non-owner account, and inspect the transaction receipt's revert data. Then decode it with the ABI to confirm caller and owner appear as typed values rather than a plain string.
When to switch
Use custom errors for new Solidity 0.8.4+ contracts, especially when you have many validation paths or want frontends to show structured revert reasons. Keep require without a string for simple guards where no revert data is needed, but prefer typed errors whenever the caller or user benefits from knowing exactly why a transaction failed.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.