Solidity Error Handling with assert(), require(), revert() – A Guide for Python Coders

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 == minter 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 type Error(string), whereas the assert() function creates an error of type Panic(uint256)
  • You can optionally add a message to the require() function, but you cannot to the assert() 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 the assert() function will run successfully.
  • If there is an error in the transfer() function call, the balance won’t be changed. But the transfer() function will raise an Exception in that case, so the assert() 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.