The nitty-gritty of Ethereum and Solidity: Error handling.

Alberto Molina
Coinmonks
6 min readFeb 24, 2024

--

Error handling in Solidity is crucial for building robust and secure decentralized applications (DApps). Solidity provides various mechanisms to handle errors and exceptions efficiently, ensuring the integrity and reliability of smart contracts. Assert, Require and Revert statements and Try/catch blocks offer developers a range of tools to validate inputs, handle unexpected conditions, and gracefully recover from failures. Understanding and implementing effective error handling practices in Solidity is fundamental for writing secure and resilient smart contracts that can withstand unforeseen circumstances in the decentralized ecosystem.

Error statements

The first thing to note is that all error statements in Solidity will revert all state changes performed within the smart contract during the reverted transaction. This helps to ensure that transactions are executed atomically and consistently, preventing unintended side effects in the event of an error or invalid condition.

If an entire transaction is reverted, any remaining gas will be refunded to the externally owned account (EOA) that initiated the transaction.

Revert

When a revert statement is executed, it immediately halts the execution of the contract, discards any modifications made to the state, and returns any remaining gas to the caller.

The revert statement can take no argument or one of the following:

  • String : the smart contract will return the string as a string of bytes to the caller. Behind the scenes what the solidity compiler is actually doing is creating an Error(string) object.
function setOddNumber(uint256 num) public returns (bool){
number = num;
if(num % 2 == 0){
revert("the provided number is not odd");
}
return true;
}

The “revert” statement will actually return this byte string : 0x08c379a0 (selector for “ERROR(string)”) + message string

  • Custom Error : see section below for more information.

The solidity compiler uses the EVM “REVERTopcode which halts the smart contract execution, reverts state changes and returns a string of bytes. The string of bytes can be empty, if no argument was provided to the “revert” statement, or it can represent either a string or a custom error.

Require

The require statement is primarily used for input validation and enforcing conditions within smart contracts. It is actually very similar to the revert statement but with two peculiarities:

  • it includes a condition as it first argument. If the condition is not met then a revert is executed with the string provided as the second parameter.
  • a string as its second parameter. This is the string that is passed to the revert triggered by the require statement when the condition is not met.

It is important to note that, as of Solidity 0.8.24, require statements do not accept custom errors.

function setOddNumber(uint256 num) public returns (bool){
require(num % 2 == 1, "provided number is not odd");
number = num;
return true;
}

Since, behind the scenes, what a require statement does when the condition is not met, is trigger a revert opcode, its behavior is identical to the revert statement : state changes are reverted, execution of the smart contract is halted and gas is refunded.

Assert

The assert statement is used for validating internal invariants and checking for conditions that should always be true during contract execution. Unlike the require statement, which is primarily used for input validation and enforcing conditions based on external inputs, assert is typically employed for internal consistency checks within the contract's logic.

The assert statement only takes one argument, a condition, if this one is not met, then the revert opcode will be executed (just like for the revert and require statements). The main difference with the other two error statements is that assert will actually return a Panic(uint256) object instead of a Error(string) or Custom Error one.

function setNonNullNumber(uint256 num) public returns (bool){
number = num;
assert(number != 0);
return true;
}

The “assert” statement will actually return this byte string : 0x4e487b71 (selector for “PANIC(uint256)”) + error code (0x01 in this use case)

The list of error codes that a “Panic” object can return is already predefined by Solidity. As of the 0.8.24 version the list is:

  1. 0x00: Used for generic compiler inserted panics.
  2. 0x01: If you call assert with an argument that evaluates to false.
  3. 0x11: If an arithmetic operation results in underflow or overflow outside of an unchecked { ... } block.
  4. 0x12; If you divide or modulo by zero (e.g. 5 / 0 or 23 % 0).
  5. 0x21: If you convert a value that is too big or negative into an enum type.
  6. 0x22: If you access a storage byte array that is incorrectly encoded.
  7. 0x31: If you call .pop() on an empty array.
  8. 0x32: If you access an array, bytesN or an array slice at an out-of-bounds or negative index (i.e. x[i] where i >= x.length or i < 0).
  9. 0x41: If you allocate too much memory or create an array that is too large.
  10. 0x51: If you call a zero-initialized variable of internal function type.

Custom Errors

Custom errors allow developers to define their own error messages and codes, providing more descriptive and meaningful feedback to users interacting with their smart contracts. Custom errors can only be used with the revert statement (as of solidity 0.8.24).

They are defined in a very similar way to events : error ErrorName(…) and they might contain one or many arguments.

error OddNumber(uint256, string);

function setOddNumber(uint256 num) public returns (uint256){
number = num;
if(num % 2 == 0){
revert OddNumber(num, "provided number is not odd");
}
return num;
}

The “revert” statement will actually return this byte string : 0x720e44a8 (selector for “OddNumber(uint256,string)”) + num (32 bytes hexadecimal format) + message string

Custom errors are added to the contract’s abi so that they can be decoded by off-chain applications.

{
"inputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
},
{
"internalType": "string",
"name": "",
"type": "string"
}
],
"name": "OddNumber",
"type": "error"
}

Try/Catch block

When a smart contract calls another one, it must manage the output returned by the “call” opcode :

  • boolean : true if the smart contract execution was successful, false if the execution hit a “revert” (or invalid) opcode.
  • bytes : response message. This is the string of bytes the revert opcode returns.

Based on the success/failure of the call, the caller smart contract should either continue or stop its execution, the decision is up to the developer.

Solidity encapsulated this complexity if we call another contract using an interface. When the callee smart contract returns an error, solidity will buble up the error to the calling smart contract, potentially reverting the whole transaction.

However, solidity also offers another tool, the “try/catch” block that can be used to managed the different types of errors / panics that can be returned by either calling an external smart contract of creating a new smart contract.

The structure of a try/catch block is as follows:

  • The try keyword has to be followed by an expression representing an external function call or a contract creation.
  • The returns part (which is optional) that follows declares return variables matching the types returned by the external call. In case there was no error, these variables are assigned and the contract’s execution continues inside the first success block.
  • The code within the try block is not “checked” (as opposed to most programming languages having a similar try/catch block structure), that code is actually executed if the expression following the try keyword succeeded.
  • After the try block, one or multiple catch blocks can be added, one for a different error type, or even one for all of them. This enables developers to process errors based on their type.
function invokeNonNullNumber(uint256 num, address add) public returns (uint256, bool) {

try IContract(add).setNonNullNumber(num) returns (bool v) {
return (0, v);
} catch Panic(uint errorCode) {
return (errorCode, false);
}
}

Conclusion

Best practices suggest that require/revert statements should be employed for checking input argument errors, while the assert statement is reserved for internal invariant checks (remains to be seen if this use case applies to your smart contracts, keep in mind that assert statements, as any other statement, costs gas).

Currently, there’s a preference for using revert over require because it allows for custom error messages. However, if Solidity enables require to support custom errors in the future, it's likely that developers will predominantly revert to using require.

As for try/catch blocks, they offer flexibility in handling various error types. However, they may not be intuitive for beginners, and their functionality might evolve in the future to support operations beyond external calls and contract creation.

--

--