0x01 为什么要关注错误处理
在智能合约开发中,错误处理看起来是个小事,但实际上它直接影响到:
- Gas 消耗:错误信息存储在链上是要花钱的
- 调试体验:好的错误信息能让问题定位事半功倍
- 合约大小:Solidity 0.8.4 之前,大量的 require 字符串会显著增加合约字节码大小
前段时间 review 团队代码的时候发现,很多同学还在用传统的 require + 字符串的方式处理错误,其实从 Solidity 0.8.4 开始,我们有了更好的选择。
0x02 传统的 require 方式
最常见的写法是这样的:
function withdraw(uint256 amount) external {
require(balances[msg.sender] >= amount, "Insufficient balance");
require(amount > 0, "Amount must be greater than zero");
require(!paused, "Contract is paused");
balances[msg.sender] -= amount;
payable(msg.sender).transfer(amount);
}
这种写法的问题:
- 每个字符串都会被存储在合约字节码中
- 字符串越长,部署成本越高
- revert 的时候,字符串会被 ABI 编码,增加 gas 消耗
0x03 Custom Errors 登场
Solidity 0.8.4 引入了 Custom Errors,用法如下:
error InsufficientBalance(address account, uint256 requested, uint256 available);
error ZeroAmount();
error ContractPaused();
function withdraw(uint256 amount) external {
if (balances[msg.sender] < amount) {
revert InsufficientBalance(msg.sender, amount, balances[msg.sender]);
}
if (amount == 0) revert ZeroAmount();
if (paused) revert ContractPaused();
balances[msg.sender] -= amount;
payable(msg.sender).transfer(amount);
}
好处很明显:
- 省 Gas:Custom Error 使用 4 字节的 selector 而不是完整字符串
- 可携带参数:能传递上下文信息,比如余额不足时告诉你实际有多少
- 类型安全:编译期就能检查参数类型
0x04 Gas 对比
我做了个简单测试,同样的逻辑:
| 方式 | 部署 Gas | revert Gas |
|---|---|---|
| require + 字符串 | ~45,000 | ~2,400 |
| Custom Error | ~32,000 | ~1,200 |
省了大约 30% 的部署成本和 50% 的 revert 成本。在 L2 上可能感知不强,但在主网上,这些都是真金白银。
0x05 实践建议
- 新项目直接用 Custom Errors,没有理由再用 require 字符串了
- 命名要有意义,让调用方看到错误名就知道发生了什么:
// 好
error SlippageExceeded(uint256 expected, uint256 actual);
// 不好
error Error1();
- 善用参数,传递足够的上下文信息:
error TransferFailed(address token, address from, address to, uint256 amount);
- 配合 NatSpec 使用,方便生成文档:
/// @notice Thrown when user tries to withdraw more than their balance
/// @param account The account attempting withdrawal
/// @param requested Amount requested
/// @param available Actual balance
error InsufficientBalance(address account, uint256 requested, uint256 available);
0x06 总结
Custom Errors 是 Solidity 错误处理的一次重要演进。它不仅省 Gas,还提供了更好的调试体验和类型安全。如果你的项目还在用 require + 字符串的老方式,是时候升级了。
唯一需要注意的是,一些老的工具链可能对 Custom Errors 的解析支持不够好,但随着生态的发展,这已经不是问题了。