错误处理是智能合约开发中至关重要的一环,它直接关系到合约的安全性、资金的可恢复性以及用户体验。Solidity 提供了多种方式来抛出和处理错误,每种方式都有其特定的使用场景和 gas 消耗特性。
一、核心概念:为何错误处理在 Solidity 中如此重要?
在以太坊中,交易是原子性的:要么完全执行成功,要么完全回滚,就像什么都没发生过一样(除了消耗的 Gas 费)。当一个错误被抛出时,当前交易的所有执行(包括状态变更和 Ether 转账)都会被撤销。然而,已消耗的 Gas 费不会被退还。这使得选择一种高效且成本低廉的错误抛出方式变得尤为重要。
二、错误抛出方式:三种主要方法
Solidity 主要提供了三种方式来抛出异常:require, revert 和 assert。它们在用途和 gas 消耗上有所不同。
1. require
require 是最常用、最通用的输入验证和条件检查语句。
- 语法:require(condition, "Optional error message");
- 作用: 检查条件 condition。如果结果为 false,则立即撤销状态变更,并回滚所有操作。可以提供一个可选的错误消息字符串。
- 适用场景:
- 验证用户输入:例如,检查传入的金额是否大于零。
- 检查前置条件:例如,在函数执行前检查调用者是否为合约所有者。
- 验证外部合约调用的返回值。
- Gas 消耗: 如果交易回滚,require 会退还所有剩余的 Gas。在 Solidity 0.8.0 之前,不带错误信息的 require 会消耗所有 Gas;0.8.0 及以后版本,无论是否带信息,都会退还剩余 Gas。
代码示例:
function deposit(uint256 amount) public payable {
require(amount == msg.value, "Sent ETH must equal amount");
require(amount > 0, "Deposit amount must be positive");
// … 存款逻辑
}
function withdraw(uint256 amount) public {
require(amount <= balances[msg.sender], "Insufficient balance");
require(address(this).balance >= amount, "Contract has insufficient funds");
// … 取款逻辑
}
2. revert
revert 提供了更灵活的错误抛出方式,特别是在复杂的条件判断中。
- 语法:if (!condition) {
revert("Error message");
}// 或者与自定义错误一起使用(推荐,更省Gas)
if (!condition) {
revert CustomError(arg1, arg2);
} - 作用: 无条件地撤销状态变更并回滚交易。它通常与 if 语句结合使用,在条件不满足时手动触发回滚。
- 适用场景:
- 当错误条件过于复杂,无法用一行 require 清晰表达时。
- 与自定义错误(Custom Errors) 结合使用,这是目前最省 Gas 的方式。
代码示例:
// 使用字符串
function complexOperation(uint256 x) public {
if (x < 10 || x > 100) {
revert("Value must be between 10 and 100");
}
// … 复杂逻辑
}
// 使用自定义错误(推荐)
error Unauthorized(address caller);
error InsufficientBalance(uint256 available, uint256 required);
function withdraw(uint256 amount) public {
if (msg.sender != owner) {
revert Unauthorized(msg.sender);
}
if (amount > balances[msg.sender]) {
revert InsufficientBalance(balances[msg.sender], amount);
}
// … 取款逻辑
}
//SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;
contract TestError{
error ZeroAddressNotAllowed(address addr);
modifier nonZeroAddress(address addr){
require(addr != address(0), "addrress is not allowed zero");
_;
}
modifier nonZeroAddress2(address addr){
if(addr == address(0)) revert ZeroAddressNotAllowed(addr);
_;
}
modifier nonZeroAddress3(address addr){
if(addr == address(0)) revert("addrress is not allowed zero");
_;
}
//770 gas
function test(address addr) public pure nonZeroAddress(addr) returns (address){
return addr;
}
//699 gas
function test2(address addr) public pure nonZeroAddress2(addr) returns (address){
return addr;
}
//792 gas
function test3(address addr) public pure nonZeroAddress3(addr) returns (address){
return addr;
}
}
3. assert
assert 用于检查那些“永远不应为假”的内部错误或不变性(Invariants)。
- 语法:assert(condition);
- 作用: 检查条件 condition,如果为 false,则意味着合约中存在 bug。
- 适用场景:
- 检查后置条件:在函数执行后,验证某个状态变量是否在预期的范围内。
- 检查不变性:例如,合约的总发行量应该等于所有用户余额的总和。
- 在 0.8.0 版本之后,检查算术溢出(现在语言已内置,很少需要手动写)。
- Gas 消耗: assert 消耗所有提供的 Gas,且不会退还。这是因为它代表了一个本不该发生的、严重的程序错误。
代码消耗示例:
function updateBalance(address user, uint256 newBalance) internal {
balances[user] = newBalance;
// 断言:更新后,总供应量这个不变性依然成立
assert(totalSupply >= balances[user]);
}
三、错误处理:try/catch
在 Solidity 0.6.0 及以上版本,引入了 try/catch 语句,用于处理外部调用中的失败。
-
作用: 当一个外部调用(如 externalContract.someFunction())失败时,阻止错误向上冒泡,并允许你在 catch 块中进行处理。
-
注意:
- 它不能用于处理同一合约内部函数的调用失败
- 无法捕获对不存在的合约调用(对一个不存在的合约的调用,EVM 不会执行);
- out of gas 错误不是程序异常,错误不能捕获
-
语法结构:
try externalContract.someFunction(arg1) returns (returnType value) {
// 成功时的逻辑:value 是返回值
} catch Error(string memory reason) {
// 这里捕获的是通过 revert("error message")
// 或 require(condition, "error message") 抛出的错误
// reason 就是那个字符串错误信息
} catch Panic(uint errorCode) {
// 这里捕获的是通过 assert() 或其他严重错误
// errorCode 是预定义的错误码
if (errorCode == 0x01) {
// assert 失败
} else if (errorCode == 0x11) {
// 算术溢出
} else if (errorCode == 0x12) {
// 除零
} else if (errorCode == 0x21) {
// 枚举类型转换错误
} else if (errorCode == 0x22) {
// 编码错误
} else if (errorCode == 0x31) {
// 空数组 pop()
} else if (errorCode == 0x32) {
// 数组越界访问
}
} catch (bytes memory lowLevelData) {
// 这里捕获的是:
// 1. 自定义错误(没有字符串消息的revert)
// 2. 函数不存在时的回退
// 3. 其他无法归类为 Error 或 Panic 的错误
}
代码示例:
//SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;
contract ExternalContract {
function mightFail(uint256 value) external pure returns (uint256) {
require(value > 10, "Value must be greater than 10");
require(value < 100, "Value must be less than 100");
return value * 2;
}
function assertExample(uint256 value) external pure returns (uint256) {
assert(value != 42); // 如果 value == 42 会触发 Panic
return value;
}
}
contract TryCatchExample {
ExternalContract public externalContract;
constructor(address _externalAddress) {
externalContract = ExternalContract(_externalAddress);
}
function testTryCatch(uint256 value) public view returns (string memory, uint256) {
//try externalContract.mightFail(value) returns (uint256 result) {
try externalContract.assertExample(value) returns (uint256 result) {
// 调用成功时的处理
return ("Success", result);
} catch Error(string memory reason) {
// 处理 require/revert 错误
return (string(abi.encodePacked("Error: ", reason)), 0);
} catch Panic(uint errorCode) {
// 处理 assert 或其他严重错误
return ("Panic occurred", errorCode);
} catch (bytes memory) {
// 处理其他未知错误
return ("Unknown error", 0);
}
}
}
interface IERC20 {
function transfer(address to, uint256 amount) external returns (bool);
}
function safeTransfer(IERC20 token, address to, uint256 amount) internal {
try token.transfer(to, amount) returns (bool success) {
require(success, "Transfer failed silently"); // 即使调用成功,也可能返回false
} catch Error(string memory reason) {
// 处理可读的错误原因
logTransferFailure(msg.sender, to, amount, reason);
} catch (bytes memory) {
// 处理无返回信息的错误
logTransferFailure(msg.sender, to, amount, "Low-level catch");
}
}
四、最佳实践与总结
为了编写出更安全、更经济的智能合约,请遵循以下最佳实践:
| 用途 | 输入验证 & 外部条件 | 复杂条件验证 & 自定义错误 | 内部错误 & 不变性检查 |
| Gas 效率 | 高(退还剩余 Gas) | 非常高(自定义错误最省 Gas) | 低(消耗所有 Gas) |
| 错误信息 | 字符串(可选) | 自定义错误(推荐)或字符串 | 无 |
| 使用频率 | 非常高 | 高(越来越流行) | 低 |
通过系统地理解和应用这些错误处理机制,我们可以构建出更加健壮、安全和用户友好的去中心化应用。

