In Solidity, you can use the require
, revert
, and assert
functions to check error conditions and handle exceptions, but they look similar at first glance, and you might get confused about how to use them. This article will explain their differences, specifically to Python developers.
We use a simple smart contract below, taken from the Solidity documentation, as an example in the following sections.
simple_currency.sol
// SPDX-License-Identifier: GPL-3.0 pragma solidity ^0.8.4; contract Coin { // The keyword "public" makes variables // accessible from other contracts address public minter; mapping (address => uint) public balances; // Events allow clients to react to specific // contract changes you declare event Sent(address from, address to, uint amount); // Constructor code is only run when the contract // is created constructor() { minter = msg.sender; } // Sends an amount of newly created coins to an address // Can only be called by the contract creator function mint(address receiver, uint amount) public { require(msg.sender == minter); balances[receiver] += amount; } // Errors allow you to provide information about // why an operation failed. They are returned // to the caller of the function. error InsufficientBalance(uint requested, uint available); // Sends an amount of existing coins // from any caller to an address function send(address receiver, uint amount) public { if (amount > balances[msg.sender]) revert InsufficientBalance({ requested: amount, available: balances[msg.sender] }); balances[msg.sender] -= amount; balances[receiver] += amount; emit Sent(msg.sender, receiver, amount); } }
How to Use require() in Solidity
Firstly, let’s focus on the following function mint in the example smart contract (line 20 – line 25):
// Sends an amount of newly created coins to an address // Can only be called by the contract creator function mint(address receiver, uint amount) public { require(msg.sender == minter); balances[receiver] += amount; }
Line 23 checks whether or not the caller of the function is the contract creator by using the require()
function.
The require
function creates an error if the expression in the brackets evaluates to False
. In this case, the variable minter contains the address of the contract creator (set in the constructor at the contract creation in line 17), so if the address of the caller (msg.sender
) is not the same as the address in the minter variable, the expression msg.sender == minte
r becomes False
and an Error is raised.
This is how the require
function is typically used. The Solidity documentation suggests using the require function for ensuring valid conditions at run time:
“It should be used to ensure valid conditions that cannot be detected until execution time. This includes conditions on inputs or return values from calls to external contracts.”
The require function generates a plain Error exception without data by default. For example, the following is an example of the error you would see on Remix IDE.
transact to Coin.mint errored: VM error: revert. revert The transaction has been reverted to the initial state.
You can optionally add a string argument to the require function to provide more information about the error. For example, the following example adds the message 'Caller is not the contract creator'
:
function mint(address receiver, uint amount) public { require(msg.sender == minter, 'Caller is not the contract creator'); balances[receiver] += amount; }
The message would appear in the error as shown below:
transact to Coin.mint errored: VM error: revert. revert The transaction has been reverted to the initial state. Reason provided by the contract: "Caller is not the contract creator".
The message would make it easier to identify what the problem was.
How to Use revert() in Solidity
Let’s look at the function send()
in the example above (line 27 – line 44).
// Errors allow you to provide information about // why an operation failed. They are returned // to the caller of the function. error InsufficientBalance(uint requested, uint available); // Sends an amount of existing coins // from any caller to an address function send(address receiver, uint amount) public { if (amount > balances[msg.sender]) revert InsufficientBalance({ requested: amount, available: balances[msg.sender] }); balances[msg.sender] -= amount; balances[receiver] += amount; emit Sent(msg.sender, receiver, amount); }
In this part, a custom error InsufficientBalance
is defined in line 30, which takes two parameters, requested and available. They will provide some details of the error when it is returned to the caller.
The revert()
function generates an instance of the error in line 36 when the if condition in line 35 evaluates to True
. In this example, the error is raised if the requested amount is greater than the sender’s balance (available amount).
As you can see, this part is similar to the require()
function in the previous section. In fact, the Solidity documentation explains that the following two statements would be semantically equivalent:
require(condition, "description");
if (!condition) revert Error("description")
Therefore, the main difference is that if you want to use a custom error type (such as InsufficientBalance
), you need to use the revert()
function; otherwise, you can use the require()
function, which will generate the built-in error type Error.
The following is an example of the error message on Remix:
transact to Coin.send errored: VM error: revert. revert The transaction has been reverted to the initial state. Error provided by the contract: InsufficientBalance Parameters: { "requested": { "value": "200" }, "available": { "value": "0" } }
The structure of if ... revert ...
might look familiar to those who already know Python. For example, if you were to write the same function in Python, you could create a custom Exception called InsufficientBalance
and raise it by the raise
statement as shown below:
send.py
balances = {} sender = 'me' class InsufficientBalance(Exception): def __init__(self, requested, available): self.requested = requested self.available = available def send(receiver, amount): if (amount > balances[sender]): raise InsufficientBalance(amount, balances[sender]) balances[sender] -= amount balances[receiver] += amount return Sent(sender, receiver, amount)
The following is an example of a simple test to check that the function send raises an exception InsufficientBalance
:
test_send.py
import pytest from send import send, sender, balances, InsufficientBalance def test_send(): balances[sender] = 100 with pytest.raises(InsufficientBalance) as e: send('you', 200) assert e.type == InsufficientBalance assert e.value.requested == 200 assert e.value.available == 100
Solidity and Python are different, but it shows you that you can leverage your existing Python knowledge when learning Solidity.
How to Use assert() in Solidity
In Solidity, there is another function called assert()
that you can use to throw an exception. It is similar to require, but there are some differences:
- The
require()
function creates an error of typeError(string)
, whereas theassert()
function creates an error of typePanic(uint256)
. - You can optionally add a message to the
require()
function, but you cannot to theassert()
function.
Generally speaking, a Panic
is generated when some internal error occurs. So, the Solidity documentation suggests only using the assert()
function for internal error checks.
“Assert should only be used to test for internal errors, and to check invariants. Properly functioning code should never create a Panic, not even on invalid external input. If this happens, then there is a bug in your contract which you should fix. Language analysis tools can evaluate your contract to identify the conditions and function calls that will cause a Panic.”
Therefore, as a simple guideline, you can use the require()
function to check conditions on inputs and the assert()
function for checking internal errors, as shown in the following example (taken from Solidity documentation):
// SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.5.0 <0.9.0; contract Sharer { function sendHalf(address payable addr) public payable returns (uint balance) { require(msg.value % 2 == 0, "Even value required."); uint balanceBeforeTransfer = address(this).balance; addr.transfer(msg.value / 2); // Since transfer throws an exception on failure and // cannot call back here, there should be no way for us to // still have half of the money. assert(address(this).balance == balanceBeforeTransfer - msg.value / 2); return address(this).balance; } }
In the example above, the assert mechanism works like this:
- If the
transfer()
function succeeds, the balance should be the original amount minus the transferred amount, so theassert()
function will run successfully. - If there is an error in the
transfer()
function call, the balance won’t be changed. But thetransfer()
function will raise an Exception in that case, so theassert()
function won’t even be executed. - It means that, in theory, it is impossible for the
assert()
function to be executed and also fail. - So, if the impossible situation occurs, it suggests a severe issue in the program.
Again, Python also has the assert
statement, which can be used for the same purpose. So, for those who know Python, it might be straightforward to understand this feature.
Summary
You can use the require, revert and assert functions to handle errors in Solidity, but they are used for different purposes.
As a simple guideline:
- Use the
require()
function to check conditions on inputs - Use the
revert()
function with if conditions to raise a user-defined error - Use the
assert()
function to check internal errors
Similar error handling structures exist in Python, so it won’t be challenging to understand them if you already know Python.
If you are interested, you can find more information in the official documentation and other resources below.
- Error handling: Assert, Require, Revert and Exceptions on docs.soliditylang.org
- Errors and the Revert Statement on docs.soliditylang.org
- Solidity Learning: Revert(), Assert(), and Require() in Solidity, and the New REVERT Opcode in the EVM by Steven McKie
- Error Handling (assert, require, revert) in Mastering Ethereum by Andreas M. Antonopoulos, Gavin Wood, Chapter 7