Difficulty: Beginner
Error handling in Solidity is fundamentally different from traditional programming languages because of the blockchain's transactional nature. When an error occurs in a Solidity function, all state changes made during that transaction are reverted - the blockchain state is rolled back as if the transaction never happened. However, the gas consumed up to the point of failure is still deducted from the sender. Understanding the different error-handling mechanisms and when to use each is critical for writing robust smart contracts.
The `require` statement is the most commonly used error-handling mechanism in Solidity. It validates conditions at runtime and reverts the transaction if the condition is false. Require is used for input validation, access control checks, and verifying external conditions. When a require fails, it returns any remaining gas to the caller (unlike assert) and can include an optional error message string. Since Solidity 0.8.26, require also supports custom errors, which are more gas-efficient than string messages. The pattern `require(condition, "Error message")` is the standard way to guard against invalid inputs and unauthorized access.
The `revert` statement unconditionally stops execution and reverts all state changes. It is functionally equivalent to `require(false, ...)` but is used when the error condition is determined through more complex logic than a simple boolean check - for example, inside an if-else block. Since Solidity 0.8.4, revert supports custom errors defined with the `error` keyword, which are significantly cheaper than string messages because they use only 4 bytes of selector data instead of encoding an entire string. Custom errors are the recommended practice for gas-efficient error handling.
The `assert` statement is reserved for checking invariants - conditions that should never be false if the code is correct. Assert failures indicate a bug in the contract code, not invalid user input. Before Solidity 0.8.0, assert consumed all remaining gas (using the INVALID opcode), making it especially punishing. From 0.8.0 onwards, assert uses the REVERT opcode and returns remaining gas, but it is still conventionally reserved for internal consistency checks. Do not use assert for input validation - that is the job of require.
Solidity also provides try/catch for handling errors from external function calls and contract creation. The try block wraps an external call, and the catch block handles specific error types: `catch Error(string memory reason)` catches require/revert with string messages, `catch Panic(uint errorCode)` catches assert failures, and `catch (bytes memory lowLevelData)` catches everything else. Try/catch only works with external calls - it cannot catch errors within the same contract. This mechanism is essential for contracts that interact with other contracts and need to handle potential failures gracefully without reverting their own state changes.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
// Custom errors (gas-efficient alternative to string messages)
error Unauthorized(address caller, address required);
error InsufficientBalance(uint256 requested, uint256 available);
error InvalidAmount();
error TransferFailed(address recipient);
error DeadlineExpired(uint256 deadline, uint256 currentTime);
contract ErrorHandling {
address public owner;
mapping(address => uint256) public balances;
uint256 public totalSupply;
event Deposit(address indexed user, uint256 amount);
constructor() {
owner = msg.sender;
}
// ============ require: Input Validation ============
function deposit() external payable {
// require with string message (traditional)
require(msg.value > 0, "Deposit must be greater than zero");
require(msg.value <= 100 ether, "Deposit exceeds maximum");
balances[msg.sender] += msg.value;
totalSupply += msg.value;
emit Deposit(msg.sender, msg.value);
}
// ============ revert with Custom Errors ============
function withdraw(uint256 _amount) external {
// Custom error with parameters (much cheaper than string messages)
if (_amount == 0) {
revert InvalidAmount();
}
if (balances[msg.sender] < _amount) {
revert InsufficientBalance({
requested: _amount,
available: balances[msg.sender]
});
}
// Effects before interactions (checks-effects-interactions pattern)
balances[msg.sender] -= _amount;
totalSupply -= _amount;
// External call
(bool success, ) = payable(msg.sender).call{value: _amount}("");
if (!success) {
revert TransferFailed(msg.sender);
}
}
// ============ assert: Invariant Checks ============
function transfer(address _to, uint256 _amount) external {
require(_to != address(0), "Cannot transfer to zero address");
require(balances[msg.sender] >= _amount, "Insufficient balance");
uint256 totalBefore = balances[msg.sender] + balances[_to];
balances[msg.sender] -= _amount;
balances[_to] += _amount;
// Assert: this should ALWAYS be true if our math is correct
// If this fails, there is a bug in the contract
assert(balances[msg.sender] + balances[_to] == totalBefore);
}
// ============ Conditional Logic with Revert ============
function executeWithDeadline(uint256 _deadline) external view {
if (block.timestamp > _deadline) {
revert DeadlineExpired({
deadline: _deadline,
currentTime: block.timestamp
});
}
if (msg.sender != owner) {
revert Unauthorized({
caller: msg.sender,
required: owner
});
}
// ... execute logic
}
// ============ try/catch: External Call Error Handling ============
interface IExternalContract {
function riskyOperation(uint256 value) external returns (uint256);
}
function safeExternalCall(
address _externalContract,
uint256 _value
) external returns (uint256 result, bool success) {
try IExternalContract(_externalContract).riskyOperation(_value) returns (
uint256 returnValue
) {
// External call succeeded
result = returnValue;
success = true;
} catch Error(string memory reason) {
// Caught a require() or revert() with a string message
// reason contains the error string
result = 0;
success = false;
} catch Panic(uint256 errorCode) {
// Caught an assert() failure or arithmetic error
// errorCode: 0x01 = assert, 0x11 = overflow, 0x12 = div by zero
result = 0;
success = false;
} catch (bytes memory lowLevelData) {
// Caught a custom error or low-level revert
result = 0;
success = false;
}
}
// Allow contract to receive ETH
receive() external payable {
balances[msg.sender] += msg.value;
totalSupply += msg.value;
}
}
This contract demonstrates all Solidity error handling mechanisms. 'require' is used for input validation and access control with string messages. 'revert' with custom errors provides gas-efficient error reporting with structured data. 'assert' checks invariants that should never fail (indicating a bug if they do). 'try/catch' handles errors from external contract calls, with separate catch blocks for different error types.
require, revert, assert, try/catch