Different types of exceptions that can occur in Solidity contracts (Error handling in Solidity, pt. 2)
100 days of solidity (Day 18–22)
Error handling in solidity
If you have not read the first part of error handling, click here to get an understanding of what it is.
In this part, we will be talking about the different types of exceptions that can occur in Solidity contracts, such as assertion failures, out-of-gas exceptions, invalid operations, and others.
The different types of exceptions that can occur in Solidity contracts
In Solidity contracts, several types of exceptions can occur during contract execution, each representing different exceptional conditions or errors. Understanding these exception types is crucial for robust error handling and contract security. Here are the main types of exceptions that can occur in Solidity:
Assertion Failure
Circumstances: Assertion failures occur when a condition specified in the contract's code using the assert
statement is not met. It indicates a violation of an assumed contract invariant or logic inconsistency. Assertion failures occur when an internal assumption made within the contract is violated.
Implications: Assertion failures are critical and often indicate bugs or unexpected behavior in the contract's code. They serve as a safety mechanism to catch unexpected conditions and halt execution to prevent further damage.
When an assertion fails, it indicates an internal error or an inconsistent state within the contract. The transaction is immediately reverted, and all state changes made in the current transaction are undone. assert is not intended for handling external inputs or user validation.
Assertion failures should be thoroughly investigated and resolved to ensure the contract's correctness and stability.
In this contract, we have a simple function withdraw
, which allows users to withdraw a specified amount from the contract's balance. Before the withdrawal, the assert
statement is used to validate that the contract has enough balance to fulfill the withdrawal request.
The assert
statement checks if the condition balance >= amount
is true. If it is not true (i.e., the contract does not have enough balance), the assertion fails, indicating an internal error or inconsistent state within the contract. When the assertion fails, the transaction is immediately reverted, and any state changes made in the current transaction are undone.
It's essential to use assert
carefully and only for checks that should never fail under normal circumstances. In this example, we use assert
to catch internal errors, such as a contract being in an unexpected state with insufficient balance for withdrawal. However, for user input validation or other external conditions, require
(we will be seeing this soon), it is generally a better choice, as it provides controlled error handling and reverts the transaction with a custom error message.
Out-of-Gas Exceptions
Circumstances: Out-of-gas exceptions occur when the computational operations performed by a contract consume more gas than the gas limit specified for the transaction. Gas is a limited resource used to execute contracts on the Ethereum network.
Solidity contracts execute within the Ethereum Virtual Machine (EVM) and consume gas for computational operations. Each transaction has a predefined gas limit. If a contract's execution exceeds the gas limit allotted for a transaction, an out-of-gas exception is triggered, and the transaction reverts. Out-of-gas exceptions are critical to consider when designing gas-efficient contracts.
Implications: When an out-of-gas exception occurs, the current transaction is reverted, and any changes made to the contract's state during the transaction are undone. Out-of-gas exceptions are crucial for preventing infinite loops or resource-exhausting operations. It emphasizes the need for gas optimization in contract development to ensure efficient and cost-effective execution.
In this contract, we have a function addElements
that performs an expensive operation by pushing elements into an array. If the value of numElements
is too large, the for
loop will consume a significant amount of gas, eventually exceeding the gas limit set for the transaction.
If the gas limit is insufficient to execute the entire loop, the transaction will run out of gas, leading to an out-of-gas exception. As a result, the transaction will be reverted, and any state changes made during the loop execution will be undone.
It's essential to consider gas consumption in your contracts, especially when performing computations or iterations over large datasets. Careful gas optimization and proper estimation of gas requirements can help prevent out-of-gas exceptions and ensure successful contract execution.
Invalid Operations
Circumstances: Invalid operations refer to operations that violate the contract's logic, constraints, or specified requirements. These can occur due to various reasons, such as:
Division by Zero: Attempting to divide a number by zero triggers an exception, as division by zero is mathematically undefined.
Array Index Out-of-Bounds: Accessing an array at an index that is outside the defined range raises an exception.
Type Conversion Issues: Invalid type conversions, such as converting incompatible data types or exceeding the range of numeric types, can result in exceptions.
Implications: Invalid operations can lead to unexpected behavior, invalid states, or vulnerabilities in contracts. They pose security risks and can be exploited by attackers to manipulate contract behavior, drain funds, or perform unauthorized operations. Proper input validation, range checking, and explicit error handling are essential to preventing and handling invalid operations securely.
This example shows
Division by Zero: We attempt to perform a division by zero by setting
b
to 0 and then dividinga
byb
. This will cause a division by zero exception, as dividing by zero is not allowed.Array Index Out-of-Bounds: We initialize an array
myArray
with three elements. Then, we try to access the element at index 3, which is out of bounds since the array only has indices 0, 1, and 2. This will cause an array index out-of-bounds exception.Type Conversion Issue: We add two
uint8
variables (num1
andnum2
) and store the result in anotheruint8
variable (result
). The sum ofnum1
andnum2
exceeds the maximum value representable byuint8
(255), leading to an invalid implicit conversion fromuint16
touint8
. This will cause a type conversion issue.
Each of these scenarios will result in the transaction being reverted, and any state changes made during the transaction execution will be undone to prevent unexpected and potentially harmful behavior.
Revert and Require Statements
Circumstances: Revert and require statements are used to explicitly trigger exceptions and revert transaction execution in specific conditions. They are commonly used for input validation, preconditions, or contract invariant checks.
revert(): The revert() function is used to explicitly revert the state changes made within a function and provide an optional error message. Reverting an operation throws an exception and rolls back the changes, ensuring the contract's state remains unchanged.
require(): The require() function is similar to revert(), but it is typically used to validate input conditions at the beginning of a function. If the condition specified in require() evaluates to false, the function execution reverts, and any state changes are undone.
Implications: Revert and require statements allow contracts to enforce specific conditions and provide custom error messages. When a revert or require condition evaluates to false, the current transaction reverts, and any changes made to the contract's state during the transaction are undone. These statements are crucial for ensuring contract integrity, security, and preventing undesired or invalid operations.
Explanation of the contract:
balance
: Represents the contract's balance, from which users can withdraw funds.owner
: Holds the address of the contract deployer, who is considered the owner.withdraw
: Function that allows users to withdraw funds from the contract balance. It usesrequire
statements to validate the withdrawal amount and ensure the contract has sufficient balance for the withdrawal. If any of these conditions fail, the function will revert, and the transaction will be rolled back.onlyOwner
: Function that can only be called by the contract owner (the address that deployed the contract). It uses arequire
statement to check if the sender is the owner. If the condition fails, the function will revert.throwError
: Function that demonstrates the use ofrevert
. It triggers a revert with a custom error message. When this function is called, it will always revert with the specified message.
Both revert
and require
statements are used for error handling in Solidity. require
is typically used for input validation and enforcing preconditions, while revert
is used to explicitly revert the transaction with a custom error message. Proper usage of these statements ensures that your contracts remain secure and provide clear feedback to users when exceptional conditions occur.
External Call Exceptions
Exceptions can also occur during interactions with other contracts through external calls. If an external call fails (e.g., due to an invalid address, out-of-gas in the called contract, or a revert in the called contract), the current transaction will be reverted, and any state changes are undone.
Explanation of the contract:
targetContract
: Represents the address of the external contract we want to call.doExternalCall
: Function that performs an external call to thetargetContract
. It uses therequire
statement to validate that thetargetContract
address is set. If the target contract address is not set (i.e., it is the zero address), the function will revert with the error message "Target contract address not set."ExternalCallExample
contract is designed to call theprocessData
function of theTargetContract
. TheprocessData
function takes auint256
parameter and returns the result of multiplying it by 2.TargetContract
: An example contract with theprocessData
function that theExternalCallExample
contract will call.
In this contract, when doExternalCall
is executed, it attempts an external call to the processData
function of the TargetContract
. The require
statement is used to check whether the external call was successful. If the external call fails (e.g., due to an invalid target contract address, out-of-gas in the called contract, or a revert in the called contract), the require
statement will cause the function to revert with the error message "External call failed."
Custom exceptions
Solidity allows developers to define custom exceptions using the error keyword. Custom exceptions can represent specific error scenarios or contract-specific conditions that may warrant different handling. They provide a way to capture domain-specific errors and make error handling more granular.
maxValue
represents the maximum value that can be set using thesetValue
function.setValue
: Function that sets a new value formaxValue
. Before setting the new value, it checks if the inputnewValue
exceeds the maximum allowed value. If the condition is met, the function will revert with the custom error message "Value exceeds the maximum allowed."
In this contract, if a caller tries to set a value greater than the maxValue
, the transaction will be reverted, and the custom error message will be provided, indicating that the value exceeds the allowed limit.
Custom errors are useful for providing clear and specific feedback to users about why a transaction was reverted, making it easier to diagnose and handle exceptional conditions in your smart contracts.
Low-Level Exceptions
Low-level exceptions are raised by the EVM itself and include exceptions such as stack overflow, invalid opcode, and stack underflow. These exceptions occur due to issues with the execution environment and are generally not directly handled within Solidity contracts. Instead, they are automatically reverted by the EVM.
Solidity also supports exception handling in the form of try/catch-statements, but only for external function calls and contract creation calls.
Okay! We've come a long way with error handling. In part 3, we will be talking about the try-catch mechanism and how it improves error handling.
If you have not read the first part of error handling, click here to get an understanding of what it is.
Check out my other articles on Solidity Basics (Solidity Data Types and Operators)**, [solidity inheritance](favourajaye.hashnode.dev/solidity-inheritance), Solidity fallback function and function overloading, Variables and control structures in solidity, Solidity Functions, Libraries in solidity, Abstract contracts and Interfaces in solidity, [Guidelines on becoming a Blockchain Developer in 2023 (Solidity)](medium.com/coinsbench/guidelines-on-becomin..), [What is blockchain?](medium.com/web3-magazine/what-is-blockchain..), [All you need to know about web 3.0](medium.com/web3-magazine/all-you-need-to-kn..), [Solidity: Floating points and precision](medium.com/@favoriteblockchain/solidity-flo..), and others
Click here to see the Github repo for this 100 days of solidity challenge.
Don’t forget to follow me, put your thoughts in the comment section, and turn on your notifications.