THE try/catch syntax introduced in version 0.6.0 is arguably the biggest advancement in error handling capabilities in Solidity, since reason chains for to come back And require were released in v0.4.22. Both to try And catch have been reserved keywords since v0.5.9 and now we can use them to handle failures in external function calls without rolling back the entire transaction (state changes in the called function are always rolled back, but those in the calling function are not).
We’re moving further away from the purist “all or nothing” approach to the transaction lifecycle, which falls far short of the practical behaviors we often desire.
Managing failed external calls
The try/catch statement allows you to react in case of failure external calls and contract creation calls, so you cannot use it to internal function calls. Note that to wrap a public function call in the same contract with try/catch, it can be made external by calling the function with This..
The example below shows how try/catch is used in a factory pattern where contract creation may fail. What follows CharitySplitter contract requires mandatory address property _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 is a factory contract… CharitySplitterFactory which is used to create and manage instances of CharitySplitter. In factory, we can pack the new CharitySplitter (charityOwner) in a try/catch as a safety when this constructor might fail due to a void CharityOwner being adopted.
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++; } } }
Note that with try/catch only exceptions occurring inside the outer call itself are caught. Errors inside the expression are not detected, for example if the input parameter for the new CharitySplitter is itself part of an internal call, errors it generates will not be detected. The example demonstrating this behavior is the modified model createCharitySplitter function. Here the CharitySplitter constructor input parameter is dynamically retrieved from another function – getCharityOwner. If this function returns, in this example with “return-required-for-testing”which will not be captured in the try/catch statement.
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 extend the try/catch logic in the createCharitySplitter function to retrieve the error message if it was issued by a failure to come back Or require and broadcast it during an event. There are two ways to achieve this:
1. Use Capture 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 emits the following event on a failing constructor requires an error:
CharitySplitterFactory.ErrorHandled( reason: 'no-owner-provided' (type: string) )
2. Use catch (memory reason in bytes)
function createCharitySplitter(address charityOwner) public { try new CharitySplitter(charityOwner) returns (CharitySplitter newCharitySplitter) { charitySplitters(msg.sender) = newCharitySplitter; } catch (bytes memory reason) { errorCount++; emit ErrorNotHandled(reason); } }
Which emits the following event on a failing constructor requires an error:
CharitySplitterFactory.ErrorNotHandled( reason: hex'08c379a0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000116e6f2d6f776e65722d70726f7669646564000000000000000000000000000000' (type: bytes)
The above two methods for retrieving the error string produce a similar result. The difference is that the second method does not ABI decode the error string. The advantage of the second method is that it is also executed if the ABI decoding of the error string fails or no reason was provided.
Future projects
There are plans to release support for error types, meaning we will be able to declare errors in the same way as events allowing us to detect different error types, for example:
catch CustomErrorA(uint data1) { … } catch CustomErrorB(uint() memory data2) { … } catch {}