这篇文章最初发表在以太坊博客.
在 0.6.0 中引入的 try/catch 语法 可以说是 Solidity 中错误处理能力的重大飞跃,因为自revert 和 require 在 v0.4.22 中发布以来,reason 字符串就一直存在。 try 和 catch 都是 自 v0.5.9 以来保留的关键字,现在我们可以使用它们来处理 external 函数调用中的失败,而不会回滚整个事务(被调用函数中的状态更改仍然会被回滚,但调用函数中的状态更改则不会)。
我们正在从事务生命周期中纯粹的“全有或全无”方法中迈出一步,这种方法无法满足我们经常需要的实际行为。
处理外部调用失败
try/catch 语句允许您对失败的 external 调用和 合约创建 调用做出反应,因此您不能将其用于 internal 函数调用。请注意,要使用 try/catch 包装同一合约中公共函数调用,可以通过 this. 调用函数将其设为外部函数。
下面的示例演示了如何在工厂模式中使用 try/catch,其中合约创建可能会失败。以下 CharitySplitter 合约在其构造函数中需要一个强制性地址属性 _owner。
pragma solidity ^0.6.1; contract CharitySplitter { address public owner; constructor (address _owner) public { require(_owner != address(0), "no-owner-provided"); owner = _owner; } }
有一个工厂合约 - CharitySplitterFactory,用于创建和管理 CharitySplitter 的实例。在工厂中,我们可以将 new CharitySplitter(charityOwner) 包装在 try/catch 中,作为当构造函数由于传递的空 charityOwner 而可能失败时的保险措施。
pragma solidity ^0.6.1; import "./CharitySplitter.sol"; contract CharitySplitterFactory { mapping (address => CharitySplitter) public charitySplitters; uint public errorCount; event ErrorHandled(string reason); event ErrorNotHandled(bytes reason); function createCharitySplitter(address charityOwner) public { try new CharitySplitter(charityOwner) returns (CharitySplitter newCharitySplitter) { charitySplitters[msg.sender] = newCharitySplitter; } catch { errorCount++; } } }
请注意,使用 try/catch,只有在外部调用本身内部发生的异常会被捕获。表达式内部的错误不会被捕获,例如,如果 new CharitySplitter 的输入参数本身是内部调用的部分,它引发的任何错误都不会被捕获。演示此行为的示例是修改后的 createCharitySplitter 函数。这里 CharitySplitter 构造函数的输入参数是从另一个函数 - getCharityOwner 动态获取的。如果该函数回滚,在本示例中,使用 "revert-required-for-testing",则不会在 try/catch 语句中捕获它。
function createCharitySplitter(address _charityOwner) public { try new CharitySplitter(getCharityOwner(_charityOwner, false)) returns (CharitySplitter newCharitySplitter) { charitySplitters[msg.sender] = newCharitySplitter; } catch (bytes memory reason) { ... } } function getCharityOwner(address _charityOwner, bool _toPass) internal returns (address) { require(_toPass, "revert-required-for-testing"); return _charityOwner; }
检索错误消息
我们可以在 createCharitySplitter 函数中进一步扩展 try/catch 逻辑,以便在失败的 revert 或 require 发出错误消息时检索该消息,并在事件中发出它。有两种方法可以实现这一点
1. 使用 catch Error(string memory reason)
function createCharitySplitter(address _charityOwner) public { try new CharitySplitter(_charityOwner) returns (CharitySplitter newCharitySplitter) { charitySplitters[msg.sender] = newCharitySplitter; } catch Error(string memory reason) { errorCount++; CharitySplitter newCharitySplitter = new CharitySplitter(msg.sender); charitySplitters[msg.sender] = newCharitySplitter; // Emitting the error in event emit ErrorHandled(reason); } catch { errorCount++; } }
在构造函数需要错误失败时发出以下事件
CharitySplitterFactory.ErrorHandled( reason: 'no-owner-provided' (type: string) )
2. 使用 catch (bytes memory reason)
function createCharitySplitter(address charityOwner) public { try new CharitySplitter(charityOwner) returns (CharitySplitter newCharitySplitter) { charitySplitters[msg.sender] = newCharitySplitter; } catch (bytes memory reason) { errorCount++; emit ErrorNotHandled(reason); } }
在构造函数需要错误失败时发出以下事件
CharitySplitterFactory.ErrorNotHandled( reason: hex'08c379a0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000116e6f2d6f776e65722d70726f7669646564000000000000000000000000000000' (type: bytes)
上面两种检索错误字符串的方法会产生类似的结果。区别在于第二种方法不会 ABI 解码错误字符串。第二种方法的优势在于它也会在 ABI 解码错误字符串失败或没有提供原因时执行。
未来计划
计划发布对错误类型的支持,这意味着我们将能够以类似于事件的方式声明错误,允许我们捕获不同类型的错误,例如
catch CustomErrorA(uint data1) { … } catch CustomErrorB(uint[] memory data2) { … } catch {}