Adopting Solidity Custom Errors: A Practical Migration Guide
A step‑by‑step guide to migrate Solidity contracts from revert strings to custom errors, covering declaration, replacement, ABI updates, testing, and gas verification.
14 Nov 2025, 02:52 UTC

Desired Outcome
Replace legacy revert strings with custom error types in an existing Solidity contract to reduce gas costs, improve ABI clarity, and enable structured error handling for off‑chain consumers.
Prerequisites
- Solidity compiler ≥ 0.8.4 (for
errordeclarations) and ≥ 0.8.7 forrequire(condition, CustomError())syntax. - Project’s
solcversion pinned inhardhat.config.jsorfoundry.toml. - Access to the contract’s ABI consumers (wallets, indexers, front‑end code) to update or verify decoding.
- Testing framework that supports error matchers (Hardhat, Foundry, Truffle).
- Optional: documentation generator that can render NatSpec tags for custom errors.
Focused Procedure
1. Declare Custom Errors
Define errors at the file or contract level. Errors can be imported from a shared module to keep the namespace clean.
// errors.sol
pragma solidity ^0.8.10;
/// @custom:errors
error Unauthorized();
error InsufficientBalance(uint256 requested, uint256 available);
2. Replace Revert Strings
Search for patterns like require(condition, "…") or revert "…" and replace with the new error types. Use revert statements for clarity and to maintain the same control flow.
import "./errors.sol";
function withdraw(uint256 amount) external {
if (msg.sender != owner) revert Unauthorized();
if (balance[msg.sender] < amount) {
revert InsufficientBalance(amount, balance[msg.sender]);
}
// ...
}
3. Update ABI‑Consuming Code
Custom errors appear in the contract ABI as entries of type: "error". Verify that the ABI is regenerated (e.g., via npx hardhat compile) and that consumers can decode the revert data. If a consumer only parses revert strings, consider adding a fallback string revert:
if (someCondition) {
revert MyError();
} else {
revert "Legacy fallback"; // only if consumer cannot decode
}
4. Add Tests for Error Matchers
Use the framework’s revert matcher to assert on the error name or selector. This guarantees the contract emits the expected error before and after the change.
it("reverts with Unauthorized", async () => {
await expect(contract.withdraw(10, { from: nonOwner }))
.to.be.revertedWithCustomError(contract, "Unauthorized");
});
5. Verify Decoding Off‑Chain
Deploy the contract to a local node, trigger a failing transaction, capture the revert data, and decode with the ABI. In Hardhat:
const tx = await contract.withdraw(10, { from: nonOwner });
await expect(tx).to.be.reverted;
const receipt = await tx.wait();
console.log(receipt.revertReason); // should be null, but receipt.revertData is available
6. Measure Gas Savings
Run identical failing calls before and after the migration using the same optimizer settings. Compare the gas used in the transaction receipt. Example with Hardhat gas reporter:
// before migration
const gasBefore = await contract.withdraw(10, { from: nonOwner }).gasLimit;
// after migration
const gasAfter = await contract.withdraw(10, { from: nonOwner }).gasLimit;
console.log(`Gas saved: ${gasBefore - gasAfter}`);
Expected Checks
- ABI contains
type: "error"entries for each declared error. - All tests that previously matched revert strings now match the error name or selector.
- Off‑chain tooling (e.g., Ethers.js, Web3.js) decodes the revert data into a readable error object.
- Gas usage for failing paths is lower by at least the size of the removed string literal.
- Documentation shows NatSpec tags for each error and the associated revert points.
Rollback Path
If a downstream consumer cannot decode the custom error, revert to the original string by restoring the require(..., "…") or revert "…" statements. Ensure the ABI is updated accordingly and re‑run all tests to confirm the fallback behavior.
Limitations & Practical Checks
- Custom errors are not human‑readable on‑chain; wallet explorers may display raw hex until they support error decoding.
- Changing an error’s name or parameters changes its selector, breaking clients that match on the selector. Treat such changes as breaking ABI updates.
- Gas savings depend on optimizer settings and the size of the string; verify with your own test suite.
- Only external calls can be caught with
try/catch; internal reverts propagate normally.
Diagram
| Step | Action |
|---|---|
| 1 | Declare error types |
| 2 | Replace revert strings |
| 3 | Regenerate ABI |
| 4 | Update tests |
| 5 | Verify decoding |
| 6 | Measure gas |
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.