Migrating Solidity Revert Strings to Custom Errors for Lower Gas and Better Error Data
Learn how to replace revert reason strings with Solidity custom errors (since 0.8.4) to cut deployment costs, keep revert semantics, and enable typed error data that off‑chain tools can decode.
11 Aug 2026, 17:16 UTC

Desired Outcome
Replace require(condition, "message") and revert "message" patterns with Solidity custom errors (available from compiler version 0.8.4) so that:
- Deployment bytecode shrinks because long string literals are removed.
- Revert data includes a 4‑byte selector plus ABI‑encoded arguments, enabling off‑chain tools to decode the error name and parameters.
- The exact revert semantics (
revertwith same data) are preserved.
Prerequisites
- Solidity version – Ensure
solc≥ 0.8.4 is used in your build configuration (e.g.,hardhat.config.jsorfoundry.toml). - Access to source – You must be able to edit the contract files and run the project’s test suite.
- Build tools – Hardhat, Foundry, or Truffle configured to compile contracts and run tests.
- Git (or equivalent) – To create a backup branch before making changes, enabling a safe rollback if needed.
Migration Procedure
1. Declare the custom error
Add the error declaration at the file level (visible to all contracts) or inside a specific contract/interface. Use descriptive names and include any data you want to return.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
// File‑level error – can be used by any contract in this file
error InsufficientBalance(uint256 available, uint256 required);
error Unauthorized(address caller);
contract Token {
mapping(address => uint256) public balance;
function transfer(address to, uint256 amount) public {
if (balance[msg.sender] < amount) {
// Use the custom error instead of a string
revert InsufficientBalance(balance[msg.sender], amount);
}
balance[msg.sender] -= amount;
balance[to] += amount;
}
function ownerOnly() public {
if (msg.sender != owner) {
revert Unauthorized(msg.sender);
}
// …
}
}
2. Replace existing require and revert statements
For each require(condition, "msg") rewrite as an if (!condition) revert CustomError(...); block. Keep the same logical condition.
// Before
require(balance[msg.sender] >= amount, "Insufficient funds");
// After
if (balance[msg.sender] < amount) {
revert InsufficientBalance(balance[msg.sender], amount);
}
If you only need a simple flag without extra data, you can define an error with no parameters:
error NonZeroAddress();
if (addr == address(0)) revert NonZeroAddress();
3. Update tests that assert on revert strings
Replace string‑based matchers with custom‑error matchers. Examples:
- Hardhat (ethers):
await expect(token.transfer(...)).to.be.revertedWithCustomError(token, "InsufficientBalance"); - Foundry:
vm.expectRevert(token.InsufficientBalance.selector); - Truffle: use
assert.revertwith a custom error decoder or upgrade to a plugin that supports selectors.
Ensure the test suite compiles and passes after the change.
4. Recompile and inspect the ABI
Run your normal compile command, then verify that the error appears in the JSON ABI.
# Hardhat
npx hardhat compile
# Foundry
forge build
Open artifacts/contracts/Token.sol/Token.json (Hardhat) or out/Token.sol/Token.json (Foundry) and confirm an entry like:
{
"type": "error",
"name": "InsufficientBalance",
"inputs": [
{"internalType": "uint256", "name": "available", "type": "uint256"},
{"internalType": "uint256", "name": "required", "type": "uint256"}
]
}
5. Measure deployment size (optional but recommended)
Compare bytecode before and after the migration.
# Hardhat
npx hardhat run scripts/size.js
# Foundry
forge inspect Token bytecode --size
You should see a reduction proportional to the length of the removed string literals.
Verification Checks
- Compiler warnings – Ensure no “unused error” or “shadowed error” messages appear.
- Test assertions** – Each revert path is exercised by a test that checks the correct error selector and decoded parameters.
- ABI presence** – Confirm the error definitions are present in the compiled contract’s ABI JSON.
- Bytecode diff** – Verify that the long string literals no longer exist in the deployment bytecode (you can grep for the original message in
bytecode). - Off‑chain decoding** – Using a library like ethers.js, call a failing transaction and decode the error:
// Example with ethers.js
try {
await token.transfer(recipient, amount);
} catch (err) {
if (err.errorName === 'InsufficientBalance') {
console.log('Available:', err.args.available.toString());
console.log('Required:', err.args.required.toString());
}
}
If the error decodes correctly, the migration succeeded.
Rollback and Recovery
Because the operation only changes source code (no state‑changing transaction is performed during the migration itself), you can revert to the previous version by:
- Checking out the Git branch or commit that existed before the changes (
git checkout previous‑branch). - Re‑compiling and re‑deploying the contracts if you had already deployed the new version.
If you have already deployed a contract that uses custom errors and need to support older off‑chain clients that expect reason strings, you must deploy a new contract version that retains the require(..., "msg") pattern or provide a wrapper contract that translates custom errors back to strings. This is a breaking change for any external parser that relied on the revert reason string.
Limitations
- Custom errors require Solidity ≥0.8.4; older compilers will reject the syntax.
- Changing from string reverts to custom errors is a breaking change for any off‑chain service that parses
revertreason strings directly. - Error selectors (the first four bytes of the keccak256 hash of the signature) can theoretically collide with function selectors; once deployed and relied upon externally, keep the error name and parameter types immutable.
- Gas saved per revert is modest (a few hundred gas); the primary benefit is reduced deployment cost and structured error data.
Summary
By declaring custom errors, replacing require/revert strings with typed revert CustomError(...) calls, updating tests, and verifying the ABI and bytecode, you achieve lower deployment costs and richer error information while preserving the exact revert behavior. Always verify with a test suite and inspect the compiled artifacts before promoting to production.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.