The try/catch syntax unveiled in 0.6.0 is arguably the most significant advancement in error management functionalities in Solidity, since reason strings for revert and require were introduced in v0.4.22. Both try and catch have been designated keywords since v0.5.9 and now we can leverage them to manage failures in external function transactions without reverting the entire transaction (state changes in the invoked function are still reverted, but those in the invoking function are preserved).
We are taking a step away from the strict “all-or-nothing” paradigm in a transaction lifecycle, which often does not align with the practical behaviour we usually desire.
Managing external call failures
The try/catch statement enables you to respond to unsuccessful external calls and contract creation attempts, hence it cannot be utilized for internal function calls. Keep in mind that to enclose a public function call within the same contract with try/catch, it can be executed externally by invoking the function with this..
The illustration below shows how try/catch is implemented in a factory model where contract creation may encounter failure. The following CharitySplitter contract necessitates a required address attribute _owner in its constructor.
pragma solidity ^0.6.1; contract CharitySplitter { address public owner; constructor (address _owner) public { require(_owner != address(0), "no-owner-provided"); owner = _owner; } }
There exists a factory contract — CharitySplitterFactory which facilitates the creation and oversight of instances of CharitySplitter. Within the factory, we can encapsulate the new CharitySplitter(charityOwner) within a try/catch as a precaution in case that constructor fails due to an absent 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++; } } }
Please note that within try/catch, only exceptions that occur inside the external invocation itself are captured. Issues occurring within the expression are not captured; for instance, if the input parameter for the new CharitySplitter is a part of an internal call, any issues it generates will not be captured. An example illustrating this behavior is the altered createCharitySplitter function. In this instance, the CharitySplitter constructor input parameter is sourced dynamically from another function — getCharityOwner. If that function fails, in this case with “revert-required-for-testing”, that will not be captured in the try/catch block.
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; }
Retrieving the error message
We can further enhance the try/catch mechanism within the createCharitySplitter function to obtain the error message if one was triggered by a failing revert or require and emit it in an event. There are two methods to accomplish this:
1. Using 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++; } }
Which triggers the subsequent event on a failed constructor require error:
CharitySplitterFactory.ErrorHandled( reason: 'no-owner-provided' (type: string) )
2. Using 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); } }
This emits the subsequent event during a failed constructor require mistake:
CharitySplitterFactory.ErrorNotHandled( reason: hex'08c379a0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000116e6f2d6f776e65722d70726f7669646564000000000000000000000000000000' (type: bytes)
The preceding two approaches for obtaining the error string yield a comparable outcome. The distinction is that the latter method does not ABI-decode the error string. The benefit of the latter method is that it also executes if ABI decoding the error string is unsuccessful or if no reason was supplied.
Future intentions
There are intentions to introduce support for error categories, meaning we will be able to declare errors in a way similar to events, enabling us to catch various types of errors, for instance:
catch CustomErrorA(uint data1) { … } catch CustomErrorB(uint[] memory data2) { … } catch {}