本文介绍了在调用智能合约时可能发生的所有错误类型,以及 Solidity 的 Try / Catch 块如何对每种错误作出响应(或无法作出响应)。
为了理解 Try / Catch 在 Solidity 中是如何工作的,我们必须了解当低级调用(low-level call)失败时会返回什么数据。这种行为是由编译器决定的,而不是由以太坊虚拟机(EVM)决定的。因此,用其他语言或汇编编写的合约不一定遵循此处解释的所有错误格式。
当对外部合约的低级调用失败时,它会返回一个布尔值 false。这个 false 表明调用未能成功执行。在以下情况下,调用可能会返回 false:
- 被调用的合约发生 revert
- 被调用的合约执行了非法操作(如除以零或访问越界的数组索引)
- 被调用的合约耗尽了所有的 gas
在允许已部署合约自毁的 EVM 兼容链上,对合约进行自毁(Self-destructing)不会导致低级调用返回 false。
在接下来的章节中,我们将探讨 10 种可能导致低级调用返回 false 的场景,以及它们可能提供的任何返回数据。
然后我们将探讨 Try / Catch 如何处理(或无法处理)每种情况。
第 1 部分:在 revert 期间会返回什么
1. 没有错误字符串的 revert 会返回什么?
使用 revert 最简单的方法是不提供 revert 的原因。
contract ContractA {
function mint() external pure {
revert();
}
}
如果我们部署上述合约(ContractA)并从另一个合约(ContractB)对 mint() 函数执行低级调用,如下所示:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
import "hardhat/console.sol";
contract ContractB {
function call_failure(address contractAAddress) external {
(, bytes memory err) = contractAAddress.call(
abi.encodeWithSignature("mint()")
);
console.logBytes(err);
}
}
将触发 revert() 错误并且不会返回任何数据,如下方截图所示:

在上面的图片中,我们可以看到该错误的返回数据是 0x,这只是一个没有任何附加数据的十六进制符号。
2. 带有错误字符串的 revert 会返回什么?
使用 revert 的另一种方法是提供一个字符串消息。这有助于识别交易在你的合约中失败的原因。
让我们按照前面的示例触发一个带有字符串的 revert,看看会返回什么:
contract ContractB {
function mint() external pure {
revert("Unauthorized");
}
}
调用合约将是:
import "hardhat/console.sol";
contract ContractA {
function call_failure(address contractBAddress) external {
(, bytes memory err) = contractBAddress.call(
abi.encodeWithSignature("mint()")
);
console.logBytes(err); // just so we can see the error data
}
}
如果我们部署这两个合约并使用 ContractB 的合约地址执行 ContractA,我们应该会得到以下结果:

当带有字符串参数触发 revert 时,它会将 Error 函数 Error(string) 的 ABI 编码返回给调用者。
我们的 revert 返回的数据将是 Error("Unauthorized") 的函数调用的 ABI 编码。
在这种情况下,它将包含 Error(string) 函数的函数选择器(function selector)、字符串的偏移量、长度以及以十六进制编码的字符串内容。

让我们进一步解释输出结果:
- 选择器
08c379a0是keccak256("Error(string)")的前四个字节,其中 string 指的是原因字符串。接下来的 96 个字节(3 行)是字符串Unauthorized的ABI 编码。 - 前 32 个字节是字符串长度位置的偏移量。
- 第二个 32 个字节是字符串的长度(12 个字节以十六进制表示为
c)。 - 字符串
Unauthorized的实际内容经过 UTF-8 编码为字节556e617574686f72697a6564。
3. 自定义 revert 会返回什么?
Solidity 0.8.4 引入了 error 类型,它可以与 revert 语句结合使用,从而创建既易读又节省 gas 的自定义错误。
要创建自定义的 error 类型,你需要使用关键字 error 来定义错误,这与定义事件(events)的方式类似:
error Unauthorized();
如果需要强调某些细节作为错误信息的一部分,你还可以定义带有参数的自定义错误:error CustomError(arg1, arg2, etc)。
error Unauthorized(address caller);
不带参数的自定义 revert
让我们比较一下带参数的自定义 revert 和不带参数的自定义 revert 的示例:
pragma solidity >=0.8.4;
error Unauthorized();
contract ContractA {
function mint() external pure {
revert Unauthorized();
}
}
在上述示例中,我们希望回滚(revert)交易并返回错误 Unauthorized。我们的调用合约将保持不变:
import "hardhat/console.sol";
contract ContractB {
function call_failure(address contractAAddress) external {
(, bytes memory err) = contractAAddress.call(
abi.encodeWithSignature("mint()")
);
console.logBytes(err); // just so we can see the error data
}
}
不带参数的自定义 revert 只会将 Unauthorized 错误的函数选择器(即 keccak256("Unauthorized()") 的前四个字节)返回给调用者,即 0x82b42900。

带有参数的自定义 revert
如果你的自定义 revert 带有参数,它将返回自定义错误函数调用的 ABI 编码。下面是一个示例:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.8.4;
error Unauthorized(address caller);
contract ContractA {
function mint() external view {
revert Unauthorized(msg.sender);
}
}
调用合约将保持不变,错误的结果将如下所示:

它仅包含自定义错误 Unauthorized(address) 的 ABI 编码。该编码包括函数选择器和 address 参数。由于 address 是静态类型,因此其编码非常直接。
其结构如下:
- 前四个字节表示函数选择器:
0x8e4a23d6 - 接下来的 32 个字节表示调用者的地址:
0000000000000000000000009c84abe0d64a1a27fc82821f88adae290eab5e07
附带提一下,你不能定义自定义的 error Error(string) 或 error Panic(uint256),因为它们会分别与 require 和 assert 返回的错误产生冲突(我们将在后面的章节中讨论 assert)。
4. require 语句引发的 revert 会返回什么?
require 语句是另一种不使用 if 语句来触发 revert 的方法。例如,你可以不这样写:
if (msg.sender != owner) {
revert();
}
而是像这样使用 require 语句:
require(msg.sender == owner);
当在没有错误消息的情况下调用 require(false) 时,它会在没有任何数据的情况下回滚(revert)交易,这类似于 revert()。最终输出的是空数据载荷(0x)。

类似于带有字符串的 revert,当触发带有字符串的 require(例如 require(false, "Unauthorized"))时,它会返回 error 函数 Error(string) 的 ABI 编码。
5. require(false, CustomError()) 会返回什么?
自 2024 年 5 月 21 日起,require 语句已经支持使用自定义错误;然而,它们目前只能通过 via-ir 使用。(请观看此描述 via-ir 的 Solidity 团队视频)。
Solidity 中的
via-ir是一个编译管道,它使用 Yul 中的中间表示(IR)来优化你的 Solidity 代码。它默认是不启用的,所以你需要使用带有solc的--via-ir标志,或者在你喜欢的开发环境中配置它。
在 Foundry 中启用 via-ir
如果你正在使用 Foundry,你只需要在 foundry.toml 配置文件中将 via-ir 设置为 true 即可激活它,如下所示:
[profile.default]
…
via-ir = true
在 Hardhat 中启用 via-ir
在 Hardhat 中,将 viaIR:true 添加到你的 hardhat.config 文件中,如下所示:
module.exports = {
solidity: {
settings: {
viaIR: true,
},
},
};
在 Remix 中启用 via-ir
如果你使用 Remix,你需要在高级编译器配置(Advance Compiler Configurations)设置中启用配置文件,如下方截图所示:

在根目录下创建一个空的 compiler_config.json 文件。并如上图所示将该路径添加到配置中。
一旦启用了“Use configuration file”选项,更新配置文件,在设置中加入 "viaIR":true,如下所示。你可能会遇到一些 lint 错误,但你的代码将成功编译。

完成这些之后,你就可以像这样在 require 中编写你的自定义错误:
require(msg.sender == owner, Unauthorized());
这与以下写法相同:
if (msg.sender != owner) {
revert Unauthorized();
}
没错,它返回的输出与我们之前讨论过的自定义 revert 完全相同。
6. assert 会返回什么?
当 assert 语句失败时,会触发 Panic(uint256) 错误。返回值是函数选择器(keccak256("Panic(uint256)") 的前 4 个字节)和错误代码的拼接结果。
下面的代码将用于说明这一点,请注意 contractB 中的 assert:
import "hardhat/console.sol";
contract ContractB {
function mint() external pure {
assert(false); // we will test what this returns
}
}
contract ContractA {
function call_failure(address contractBAddress) external {
(, bytes memory err) = contractBAddress.call(
abi.encodeWithSignature("mint()")
);
console.logBytes(err);
}
}
当我们部署并执行该合约时,我们将得到如下所示的 assert 错误:

变量 err 将保存以下数据:
0x4e487b71 // <- the function selector
0000000000000000000000000000000000000000000000000000000000000001 // the error code
4e487b71 是 keccak256("Panic(uint256)") 的前四个字节,其中 uint256 指的是错误代码。在这种情况下,错误代码为 1。我们将在下一节中看到其他错误代码。
7. 非法操作会返回什么?
就像 assert 语句一样,当发生诸如除以零、弹出空数组或数组越界错误等非法操作时,交易会触发 panic,并返回函数选择器——即 keccak256("Panic(uint256)") 的前 4 个字节——与 uint256 错误代码的拼接。
下面是一个非法操作的示例;数组越界——下面 ContractB 中的 outOfbounds() 函数所操作的 numbers 数组仅有 3 个元素。
import "hardhat/console.sol";
contract ContractB {
uint256[] numbers;
constructor() {
numbers.push(1);
numbers.push(2);
numbers.push(3);
}
function outOfbounds(uint256 index) public view returns (uint256) {
return numbers[index];
}
}
contract ContractA {
function call_failure(address contractBAddress) external {
(, bytes memory err) = contractBAddress.call(
abi.encodeWithSignature("outOfbounds(uint256)", 10)
);
console.logBytes(err);
}
}
如果我们试图访问第 10 个元素——它当然不存在,我们将会得到数组越界错误:

变量 err 将保存:
0x4e487b71 //<- function selector for Panic(uint256)
0000000000000000000000000000000000000000000000000000000000000032 // <-the error code
0x32 是数组越界错误的错误代码。
再看一个例子,如果我们试图除以零会怎样?
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
contract ContractB {
function divide(uint256 a, uint256 b) public pure returns (uint256) {
return a / b;
}
}
……然后使用参数 10 和 0 调用函数 divide:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
import "hardhat/console.sol";
contract ContractA {
function call_failure(address contractBAddress) external {
(, bytes memory err) = contractBAddress.call(
abi.encodeWithSignature("divide(uint256, uint256)", 10, 0)
);
console.logBytes(err);
}
}
结果将是相同的函数选择器,以及除以零的错误代码 0x12。

8. 在 Solidity 层面与在汇编层面的除以零操作分别返回什么?
在 Solidity 层面除以零会触发带有错误代码 18(0x12)的 revert。然而,在汇编(assembly)层面除以零不会发生 revert,相反,它会返回 0。这是因为编译器在 Solidity 层面插入了检查,而在汇编层面并没有进行这种检查。

如果你在汇编层面执行除法操作,请确保检查分母。如果它为零,请触发 revert 以回滚交易。
function divideByZeroInAssembly(uint256 numerator, uint256 denominator)
public
pure
returns (uint256 result)
{
assembly {
if iszero(denominator) {
revert(0, 0)
}
result := div(numerator, denominator)
}
}
但这种错误不会像 Solidity 中普通的除以零那样被处理,后者会以十进制为 18 或十六进制为 0x12 的错误代码触发 panic。
如果你使用 OpenZeppelin,你可以利用 OZ 的自定义 Panic 工具使用你的自定义错误代码来触发 Panic,如下所示:

有了这个,你就可以模拟 assert 在 Solidity 层面的正常行为了。
错误代码(Error Codes)
根据 Solidity 官方文档,以下错误代码 指的是可能发生的不同类型的 panic,如下方截图所示:

9. 在 out-of-gas 时会返回什么?
在低级调用发生 gas 耗尽(out-of-gas)的错误情况下,不会向调用合约返回任何内容。没有数据,也没有错误消息。
contract E {
function outOfGas() external pure {
while (true) {}
}
}
contract C {
function call_outOfGas(address e) external returns (bytes memory err) {
(, err) = e.call{gas: 2300}(abi.encodeWithSignature("outOfGas()"));
}
}
变量 err 将为空。由于 gas 的 63/64 规则,合约 C 仍将保留原来 gas 的 1/64,因此即便是你试图将所有可用的 gas 转发给合约 E,对于 call_outOfGas 函数的交易本身也不一定会因为 gas 耗尽而发生 revert。
10. 在 assembly revert 期间会返回什么?
与 Solidity 的 revert 相比,使用汇编(assembly)revert 可以让你在 gas 成本上更高效地返回错误数据。
汇编中的 revert 接受两个参数:内存槽(memory slot)和以字节为单位的数据大小(size of the data):
revert(startingMemorySlot, totalMemorySize)
你可以精确控制汇编 revert 返回什么错误数据。例如,我们能够选择使用从 delegatecall 返回的错误消息来执行 revert,只需通过 returndatasize() 确定返回数据的总内存大小即可,就像 OpenZeppelin 的 Proxy.sol 所做的那样:
function _delegate(address implementation) internal {
assembly {
calldatacopy(0, 0, calldatasize())
let result := delegatecall(
gas(),
implementation,
0,
calldatasize(),
0,
0
)
returndatacopy(0, 0, returndatasize())
if iszero(result) {
revert(0, returndatasize())
}
return(0, returndatasize())
}
}
利用低级汇编,让我们模拟 Solidity 的 revert 语句及其返回数据,以便更好地理解在汇编 revert 中返回数据的结构。
模拟没有原因字符串的 revert
与 Solidity 中的 revert() 类似,revert(0,0) 是内联汇编(Inline assembly)中的等价写法。它不返回任何错误数据,因为起始内存槽被定义为 0,并且数据大小也为 0,这表示不应该返回任何数据。
contract ContractB {
function revertWithAssembly() external pure {
assembly {
revert(0, 0) // no returned data
}
}
}
模拟带有原因字符串的 revert
在 Solidity 中使用原因的 Revert —— revert(string) 在底层涉及多个步骤:
Error(string)的 ABI 编码- 分配内存以存储字符串的元数据,如长度和偏移量
- 并为实际的字符串分配内存。
所有这些步骤都会增加 gas 成本。
为了优化 gas 成本,你可以使用汇编实现类似的功能。这种方法减少了所需的步骤和操作码(opcodes),因为我们确切地知道并控制数据是如何存储的,同时仍然返回相同的错误数据。在下面的示例中,我们手动操作了内存并直接存储了:
Error(string)的函数选择器 —— 我们可以在合约外部获取选择器然后直接使用它。为了清晰起见,我在示例中添加了该编码。- 偏移量(offset)
- 字符串长度
- 实际的字符串
- 并触发了 revert
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
contract ContractB {
function revertwithAssembly() external pure {
bytes4 selector = bytes4(abi.encodeWithSignature("Error(string)")); //selector with leading zeros
assembly {
mstore(0x00, selector) //- Store the function selector for `Error(string)`
mstore(0x04, 0x20) //- Store the offset to the error message string
mstore(0x24, 0xc) //- Store the length of the error message string
mstore(0x44, "Unauthorized") //- Store the actual error message
revert(0x00, 0x64) //- Trigger the revert revert(StartingMemorySlot, totalMemorySize)
}
}
}
当我们从这个外部合约中调用该合约时:
import "hardhat/console.sol";
contract ContractA {
function call_failure(address contractBAddress)
external
returns (bytes memory err)
{
(, err) = contractBAddress.call(
abi.encodeWithSignature("revertwithAssembly()")
);
console.logBytes(err);
}
}
结果将是以十六进制编码的数据:

这与我们使用 Solidity revert(string) 时得到的结果完全相同。
模拟带有自定义错误的 revert
为了进一步节省 gas,我们可以使用汇编来模拟自定义 revert。汇编中的自定义 revert 与 Solidity 自定义 revert 一样返回函数选择器。
然而,与 Solidity 中进行自定义错误的完整编码不同,我们可以省略这一步,只需直接存储选择器并触发 revert 即可。
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
import "hardhat/console.sol";
contract ContractB {
function customRevertWithAssembly() external pure {
bytes32 selector = bytes32(abi.encodeWithSignature("Unauthorized()"));
assembly {
mstore(0x00, selector) //- Store the function selector for the custom error
revert(0x00, 0x04)
}
}
}
contract ContractA {
function call_failure(address contractBAddress)
external
returns (bytes memory err)
{
(, err) = contractBAddress.call(
abi.encodeWithSignature("customRevertWithAssembly()")
);
console.logBytes(err);
}
}
当我们运行它时,应该能看到选择器作为返回值出现,如下所示:

Solidity 合约所有触发 revert 方式的总结
当交易通过带有原因字符串的 require 语句或包含字符串的 revert 进行回滚时,错误的返回值是 Error(string) 的选择器,紧随其后的是原因字符串的 ABI 编码。
当交易由于 assert 或非法操作发生 revert 时,错误数据是 Panic(uint256) 的选择器,紧随其后的是 ABI 编码为 uint256 的错误代码。
当遇到以下情况时,错误数据为空:
- 交易因没有原因字符串的
require()或revert()语句而发生 revert - 被调用的合约耗尽了 gas
- 被调用的合约使用汇编,并通过
revert(0, 0)触发 revert
第 2 部分:try/catch 如何处理每种情况
在本指南的第一部分,我们已经看到了不同类型的 revert 是如何以不同方式返回错误的。现在,让我们来探讨 Solidity 中的 try/catch 语句是如何处理每一种情况的。
try/catch 语句提供了一种结构化的方法,用来处理在外部函数调用或交互过程中可能发生的异常,从而避免整个交易发生 revert 并回滚。不过,如果发生错误,被调用合约中的状态改变仍然会 revert。
try/catch 语句
这是 try/catch 语句的一个典型结构。请注意,这是对 Solidity 可能触发 revert 的所有方式进行模式匹配:
function callContractB() external view {
try functionFromAnotherContract() {
//<-- Handle the success case if needed
} catch Panic(uint256 errorCode) {
//<-- handle Panic errors
} catch Error(string memory reason) {
//<-- handle revert with a reason
} catch (bytes memory lowLevelData) {
//<-- handle every other errors apart from Panic and Error with a reason
}
}
我们之前讨论过的不同类型的 revert 可以根据其返回值在 try/catch 块的不同部分中被捕获。
catch Error(string memory reason) 块处理所有带有原因字符串的 revert。这意味着,revert(string) 和 require(false, “reason”) 错误都会在这里被捕获。这是因为这些错误在触发时会返回 Error(string) 错误。
catch Panic(uint256 errorCode) 将捕获所有的非法操作,例如在 Solidity 层面除以零,以及 assert 错误,因为这些错误在触发时返回 Panic(uint256 errorCode)。
最后,任何不返回 Panic 或 Error 的其他错误都将在通用的 catch 块 catch (bytes memory lowLevelData) 中被捕获,这包括自定义错误以及没有消息字符串的错误。
如果你对错误数据不感兴趣,也可以使用 catch{ } 块。它将会捕获来自被调用合约的任何错误。
让我们来看一个 try/catch 语法的示例。在这个简单的例子中,我们将尝试模拟不同类型的错误,并编写一个 try/catch 块来根据它们的错误返回值进行处理。
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
contract ContractB {
error CustomError(uint256 balance);
uint256 public balance = 10;
function decrementBalance() external {
require(balance > 0, "Balance is already zero");
balance -= 1;
}
function revertTest() external view {
if (balance == 9) {
// revert without a message
revert();
}
if (balance == 8) {
uint256 a = 1;
uint256 b = 0;
// This is an illegal operation and should cause a panic (Panic(uint256)) due to division by zero
a / b;
}
if (balance == 7) {
// revert with a message
revert("not allowed");
}
if (balance == 6) {
// revert with a message
revert CustomError(100);
}
}
}
处理这些错误的 try/catch 块调用看起来将如下所示:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
import "hardhat/console.sol";
import {ContractB} from "contracts/revert/contractB.sol";
contract ContractA {
event Errorhandled(uint256 balance);
ContractB public contractB;
constructor(address contractBAddress) {
contractB = ContractB(contractBAddress);
}
function callContractB() external view {
try contractB.revertTest() {
// Handle the success case if needed
} catch Panic(uint256 errorCode) {
// handle illegal operation and `assert` errors
console.log("error occurred with this error code: ", errorCode);
} catch Error(string memory reason) {
// handle revert with a reason
console.log("error occured with this reason: ", reason);
} catch (bytes memory lowLevelData) {
// revert without a message
if (lowLevelData.length == 0) {
console.log("revert without a message occured");
}
// Decode the error data to check if it's the custom error
if ( bytes4(abi.encodeWithSignature("CustomError(uint256)")) == bytes4(lowLevelData)
) {
// handle custom error
console.log("CustomError occured here");
}
}
}
}
在每个 catch 块中,我们都模拟了对不同错误类型的处理。为了更好地理解,我添加了注释来解释每个块中发生的事情。
注意,这里没有专门用于自定义错误的 catch 块,比如 catch CustomError {}。相反,我们在通用的捕获所有错误的块中通过手动流程来处理它,因为目前还没有官方方法来解码自定义错误。
关于这个问题也有一个 open issue,其中包含了许多如何通过在最后的 catch 块中添加 if 语句来匹配选择器以进行 hack 处理的建议。try/catch 的 发布博客提到,未来有计划改进 try/catch 语句,以便妥善处理自定义错误。

在我们的案例中,我们检查了低级错误数据是否与我们试图捕获的特定自定义错误签名相对应。
if (bytes4(abi.encodeWithSignature("CustomError(uint256)")) == bytes4(lowLevelData)
){}
在哪些场景下 try/catch 会无法处理你的错误?
由于 try / catch 语法只能捕获来自外部合约的错误:
1) 发生在 try 或 catch 块内(在调用合约中)的任何错误都不会被捕获。
例如,这张图片中的任何一个 revert 都不会被捕获(通过 remix 运行):

2) 如果合约没有正确的“catch”类型
例如,如果合约因 panic 发生 revert,但只有一个 Error catch 块而没有通用的 catch 块,如下方截图所示(通过 Remix 运行):

3) 接口期望返回数据但并未提供时发生 Revert
如果定义另一个合约调用的接口期望返回数据,但该合约并未返回任何数据,或者返回了意外的格式,那么整个交易将会 revert 并且不会被捕获,如下面的示例所示(通过 Remix 运行):

Solidity 中 try/catch 的问题
try/catch 的问题一直是一个讨论热点,出现了许多提案,我们在本指南中也已经看到了其中一些内容。
讨论中强调的问题包括:
语法引发的错误期望
有一种误解认为 try/catch 语法在 Solidity 中的工作原理与其他语言一样,正如我们已经看到的,它是不一样的。例如,你会期望下面的代码流能够正常工作。
try <expression> {
revert();
} catch {
// also handle the revert in the try block
}
但它无法如预期般工作。catch 块并不会捕获这个 revert。它会直接终止整个交易。
缺乏处理编译器生成的检查中所发生 revert 的机制
当我们对来自另一个合约的函数进行高级调用(high-level calls)时,Solidity 编译器会对被调用合约执行多项检查,例如:
- 检查目标合约的
extcodesize,以确认目标是否为合约。如果该地址不是合约,则检查失败。 - 检查
returndatasize—— 如果方法期望返回某些数据,它会验证returndatasize是否非空。如果有返回值,它会对它们进行解码并验证它们的编码是否正确。 - 它还执行编码和解码检查。调用方也将尝试对返回的数据进行 ABI 解码,如果数据格式不正确或不存在,则会发生 revert。
如果这些检查中的任何一项失败,try/catch 语法都将无法捕获到该错误。
缺失的功能
除此之外,除了 catch Panic(uint256 errorCode) 和 catch Error(string memory reason),拥有允许你捕获如 catch CustomError() 这种自定义错误的功能,也是在 try/catch 语法中自然期望拥有的特性。然而,目前并没有针对这些错误情况的语法,它们必须在 catch 块中手动处理。
解决 Solidity 中 try/catch 问题的建议方案
从我们前面提到的提案讨论中,以下是建议方案的简要总结(在撰写本文时这些方案尚未实现):
- 使用附加功能扩展
try/catch语法,以明确定义你正在处理的错误类型。例如,internalcatch 将处理由编译器添加的额外检查触发的本地 revert(它仍然不会捕获在同一个合约内部触发的 revert),而externalcatch 将继续按照现有的 catch 实现运作。 - 为本地 revert 添加新的 catch 子句,例如:
- catch NoContract {}
- catch DecodingFailure {}
- catch Other {}
tryCall()和match—— 预计该功能将对外部函数运行模式匹配,并允许你根据结果和错误类型在 match 构造的不同分支中处理各种错误。以下是来自proposal的一个示例:
import { tryCall } from "std/errors";
error MyError(string);
match tryCall(token.transfer, (exampleAddress, 100)) {
CallSuccess(transferSuccessful) => {
...
}
CallFailure(MyError(reason)) => {
...
}
NotAContract => {
...
}
DecodingFailure(errorCode) => {
...
}
}
这项提案发布于 2023 年 2 月。因此,我们期待着最终胜出并在未来得到实施的方案。
结论
当一个 Solidity 合约发生 revert 时,它可以返回一个 ABI 编码的 Error(string)、Panic(uint256)、一个 4 字节的自定义错误,或者什么都不返回。try-catch 具有处理 Error(string)、Panic(uint256) 以及通用的 catch 块。它无法原生处理自定义错误。
如果调用者发生 revert,或者被调用者以调用者未预期的格式返回数据(比如试图解析空数据或格式不正确的数据),Try catch 就会失效。
作者信息
本文由 Eze Sunday 与 RareSkills 合作撰写。
原发布于 2024 年 7 月 25 日