Out of gas on deployment: should a low-traffic contract trade revert reason strings for custom errors?
0 reputation · 14 Sept 2025, 13:46 UTC
0 reputation · 14 Sept 2025, 13:46 UTC
I am finalizing a small Solidity contract that will be deployed once and called only occasionally, so the dominant cost is deployment rather than per-call execution. Since an out-of-gas failure consumes all supplied gas while reverting state, I want the deployed bytecode as small as reasonably possible before I commit to a compiler configuration.
The documented tension I cannot resolve is around error handling. Custom errors (available since Solidity 0.8.4) are cheaper than revert reason strings, because strings are stored in the deployed bytecode and ABI-encoded at runtime. But switching to custom errors means off-chain tooling must decode a 4-byte selector instead of reading a human-readable message, and identical selectors across contracts can be misattributed by decoders.
There is also the optimizer runs parameter, which trades deployment cost against call cost; with uncertain future call volume I am unsure what value is defensible.
My questions:
29275 reputation · 15 Sept 2025, 00:18 UTC
Yes, you should trade revert reason strings for custom errors. For a contract where deployment cost is the primary concern, the savings are meaningful because every character in a revert string increases the deployed bytecode size. Custom errors replace these strings with a 4-byte selector, significantly reducing the contract's footprint and the associated deployment gas.
The difference in gas consumption is rooted in how the Ethereum Virtual Machine (EVM) handles data:
While the runtime gas saving per call is also lower for custom errors, the primary win for low-traffic contracts is the reduction in the CREATE or CREATE2 transaction cost.
The loss of human-readable strings in the bytecode does not mean the loss of readability for the user. Modern development workflows handle this via the ABI:
The runs parameter tells the compiler whether to optimize for deployment cost (low runs) or runtime cost (high runs). For a low-traffic contract where deployment is the dominant cost, a low value (e.g., 200) is the standard defensive default. This instructs the compiler to avoid duplicating code to save gas on calls, thereby keeping the bytecode smaller.
To quantify the exact savings for your specific contract, perform the following checks:
require(condition, "Error Message") and once with if (!condition) revert CustomError(). Compare the length of the resulting bytecode hex string.gasUsed field of the deployment transaction.Missing Diagnostic: To provide a more precise optimizer recommendation, it would be necessary to know if your contract contains many small, repeated internal functions, as this significantly changes how the runs parameter affects bytecode size.
Use comments to ask for clarification. Post a solution as an answer.
29,275 reputation · 14 Sept 2025, 21:55 UTC
Each non‑zero byte in a contract’s initcode costs 200 gas during creation (zero bytes cost 4 gas). A typical ASCII revert string of 32 bytes therefore consumes roughly 6 400 gas, while a custom error’s 4‑byte selector uses about 800 gas. Replacing one such string with a selector saves ≈5 600 gas; five errors cut deployment cost by ~28 000 gas, a noticeable amount on mainnet. This saving is additive to any optimizer‑runs tweak.