Error handling is a key part of smart contract development. Solidity provides multiple error handling mechanisms, and since Solidity 0.8.4, custom errors were introduced, significantly optimizing Gas consumption and error message readability.
1. Error Handling Mechanism Overview
solidity/* Error handling in Solidity: 1. require(condition, message) - Used for validating input and external conditions - Reverts all state changes on failure - Refunds remaining Gas 2. assert(condition) - Used for checking internal invariants - Failure indicates a bug in the code - Consumes all Gas (serious error) 3. revert(message) / revert CustomError() - Explicitly reverts transaction - Solidity 0.8.4+ supports custom errors 4. try/catch - Catches external call exceptions - Supported since Solidity 0.6.0 */
2. Using require
soliditycontract RequireExample { mapping(address => uint256) public balances; // Basic usage function transfer(address _to, uint256 _amount) external { require(_to != address(0), "Invalid recipient address"); require(_amount > 0, "Amount must be greater than 0"); require(balances[msg.sender] >= _amount, "Insufficient balance"); balances[msg.sender] -= _amount; balances[_to] += _amount; } // Multiple condition checks function complexOperation( address _token, uint256 _amount, uint256 _minAmount, uint256 _deadline ) external { require(_token != address(0), "Invalid token"); require(_amount >= _minAmount, "Amount below minimum"); require(_amount <= 10000 ether, "Amount exceeds maximum"); require(block.timestamp < _deadline, "Transaction expired"); require( _amount % 100 == 0, "Amount must be multiple of 100" ); // Execute operation... } // require with error message function withdraw(uint256 _amount) external { uint256 balance = balances[msg.sender]; require( balance >= _amount, string(abi.encodePacked( "Insufficient balance. Available: ", uint2str(balance), ", Requested: ", uint2str(_amount) )) ); balances[msg.sender] -= _amount; payable(msg.sender).transfer(_amount); } // Helper function: uint to string function uint2str(uint256 _i) internal pure returns (string memory) { if (_i == 0) return "0"; uint256 j = _i; uint256 length; while (j != 0) { length++; j /= 10; } bytes memory bstr = new bytes(length); uint256 k = length; j = _i; while (j != 0) { bstr[--k] = bytes1(uint8(48 + j % 10)); j /= 10; } return string(bstr); } }
3. Using assert
soliditycontract AssertExample { uint256 public constant MAX_SUPPLY = 1000000; uint256 public totalSupply; mapping(address => uint256) public balances; // assert is used for checking internal invariants function mint(address _to, uint256 _amount) external { // Use require to check external input require(_to != address(0), "Invalid address"); require(_amount > 0, "Invalid amount"); uint256 newTotalSupply = totalSupply + _amount; require(newTotalSupply <= MAX_SUPPLY, "Exceeds max supply"); // State update totalSupply = newTotalSupply; balances[_to] += _amount; // Use assert to check internal invariants // If it fails, there's a serious bug in the code assert(totalSupply >= balances[_to]); assert(balances[_to] >= _amount); } // Invariant check after mathematical operations function divide(uint256 a, uint256 b) external pure returns (uint256) { require(b > 0, "Division by zero"); uint256 result = a / b; // Check mathematical invariant assert(result * b <= a); return result; } // State check after complex operations function complexTransfer( address _from, address _to, uint256 _amount ) external { uint256 fromBalanceBefore = balances[_from]; uint256 toBalanceBefore = balances[_to]; // Execute transfer balances[_from] -= _amount; balances[_to] += _amount; // Check invariants assert(balances[_from] == fromBalanceBefore - _amount); assert(balances[_to] == toBalanceBefore + _amount); assert( balances[_from] + balances[_to] == fromBalanceBefore + toBalanceBefore ); } }
4. Using revert
soliditycontract RevertExample { // Custom errors (Solidity 0.8.4+) error InsufficientBalance(uint256 available, uint256 required); error InvalidAddress(address provided); error Unauthorized(address caller, bytes32 requiredRole); error TransferFailed(address from, address to, uint256 amount); error DeadlineExpired(uint256 deadline, uint256 currentTime); error SlippageExceeded(uint256 expected, uint256 actual); mapping(address => uint256) public balances; mapping(address => bytes32) public roles; // Using custom errors (Gas optimization) function transferWithCustomError( address _to, uint256 _amount ) external { if (_to == address(0)) { revert InvalidAddress(_to); } uint256 balance = balances[msg.sender]; if (balance < _amount) { revert InsufficientBalance(balance, _amount); } balances[msg.sender] -= _amount; balances[_to] += _amount; } // Custom error with multiple parameters function swapTokens( address _tokenIn, address _tokenOut, uint256 _amountIn, uint256 _minAmountOut, uint256 _deadline ) external { if (block.timestamp > _deadline) { revert DeadlineExpired(_deadline, block.timestamp); } // Simulate swap calculation uint256 amountOut = _amountIn * 95 / 100; // 5% slippage if (amountOut < _minAmountOut) { revert SlippageExceeded(_minAmountOut, amountOut); } // Execute swap... } // Permission check function adminFunction() external view { bytes32 requiredRole = keccak256("ADMIN_ROLE"); if (roles[msg.sender] != requiredRole) { revert Unauthorized(msg.sender, requiredRole); } // Execute admin operation... } // Legacy revert (string message) function legacyRevert(address _to, uint256 _amount) external { if (_to == address(0)) { revert("Transfer to zero address"); } if (balances[msg.sender] < _amount) { revert("Insufficient balance"); } // Execute transfer... } }
5. Gas Optimization with Custom Errors
soliditycontract GasComparison { // Custom error error CustomError(uint256 code); // Using require string (consumes more Gas) function useRequire(uint256 _value) external pure { require(_value > 0, "Value must be greater than zero"); require(_value < 1000, "Value must be less than 1000"); require(_value % 2 == 0, "Value must be even"); } // Using custom errors (saves Gas) function useCustomError(uint256 _value) external pure { if (_value == 0) revert CustomError(1); if (_value >= 1000) revert CustomError(2); if (_value % 2 != 0) revert CustomError(3); } /* Gas comparison: - require string: ~200-300 gas + string storage cost - custom error: ~50-100 gas In frequently called functions, custom errors can significantly save Gas */ } // Practical custom error design contract TokenWithCustomErrors { // Define all possible errors error TransferFromZeroAddress(); error TransferToZeroAddress(); error TransferAmountExceedsBalance(address sender, uint256 balance, uint256 amount); error ApproveFromZeroAddress(); error ApproveToZeroAddress(); error InsufficientAllowance(address owner, address spender, uint256 allowance, uint256 amount); error MintToZeroAddress(); error BurnFromZeroAddress(); error BurnAmountExceedsBalance(address account, uint256 balance, uint256 amount); error InvalidSender(address sender); error InvalidReceiver(address receiver); error PermitDeadlineExpired(uint256 deadline, uint256 currentTime); error InvalidPermitSignature(address signer, address owner); mapping(address => uint256) private _balances; mapping(address => mapping(address => uint256)) private _allowances; uint256 private _totalSupply; function transfer(address _to, uint256 _amount) external returns (bool) { address owner = msg.sender; if (owner == address(0)) revert TransferFromZeroAddress(); if (_to == address(0)) revert TransferToZeroAddress(); uint256 fromBalance = _balances[owner]; if (fromBalance < _amount) { revert TransferAmountExceedsBalance(owner, fromBalance, _amount); } _balances[owner] = fromBalance - _amount; _balances[_to] += _amount; return true; } function transferFrom( address _from, address _to, uint256 _amount ) external returns (bool) { if (_from == address(0)) revert TransferFromZeroAddress(); if (_to == address(0)) revert TransferToZeroAddress(); uint256 currentAllowance = _allowances[_from][msg.sender]; if (currentAllowance < _amount) { revert InsufficientAllowance(_from, msg.sender, currentAllowance, _amount); } _allowances[_from][msg.sender] = currentAllowance - _amount; uint256 fromBalance = _balances[_from]; if (fromBalance < _amount) { revert TransferAmountExceedsBalance(_from, fromBalance, _amount); } _balances[_from] = fromBalance - _amount; _balances[_to] += _amount; return true; } function mint(address _to, uint256 _amount) external { if (_to == address(0)) revert MintToZeroAddress(); _totalSupply += _amount; _balances[_to] += _amount; } function burn(uint256 _amount) external { address account = msg.sender; if (account == address(0)) revert BurnFromZeroAddress(); uint256 accountBalance = _balances[account]; if (accountBalance < _amount) { revert BurnAmountExceedsBalance(account, accountBalance, _amount); } _balances[account] = accountBalance - _amount; _totalSupply -= _amount; } }
6. try/catch Exception Handling
soliditycontract TryCatchExample { // External contract interface interface IExternalContract { function riskyOperation(uint256 _value) external returns (uint256); function anotherFunction() external view returns (bool); } // Custom errors error ExternalCallFailed(address target, bytes reason); error ExternalCallReverted(address target, string reason); error ExternalCallPanic(address target, uint256 code); // Using try/catch to handle external calls function safeExternalCall( address _target, uint256 _value ) external returns (bool success, uint256 result) { try IExternalContract(_target).riskyOperation(_value) returns (uint256 _result) { // Call successful return (true, _result); } catch Error(string memory reason) { // Catch revert("string") errors revert ExternalCallReverted(_target, reason); } catch Panic(uint256 errorCode) { // Catch assert failures or internal errors // errorCode: // 0x01: assert failure // 0x11: arithmetic overflow/underflow // 0x12: division by zero // 0x21: conversion to invalid enum // 0x22: accessing incorrectly encoded storage byte array // 0x31: pop from empty array // 0x32: array out of bounds access // 0x41: allocating too much memory // 0x51: calling uninitialized internal function revert ExternalCallPanic(_target, errorCode); } catch (bytes memory lowLevelData) { // Catch custom errors or other low-level errors revert ExternalCallFailed(_target, lowLevelData); } } // Batch calls, partial failures don't affect others function batchExternalCalls( address[] calldata _targets, uint256[] calldata _values ) external returns (bool[] memory successes, uint256[] memory results) { require(_targets.length == _values.length, "Length mismatch"); successes = new bool[](_targets.length); results = new uint256[](_targets.length); for (uint i = 0; i < _targets.length; i++) { try IExternalContract(_targets[i]).riskyOperation(_values[i]) returns (uint256 result) { successes[i] = true; results[i] = result; } catch { // Record failure, continue executing other calls successes[i] = false; results[i] = 0; } } return (successes, results); } // External call with retry function callWithRetry( address _target, uint256 _value, uint256 _maxAttempts ) external returns (bool success, uint256 result) { for (uint i = 0; i < _maxAttempts; i++) { try IExternalContract(_target).riskyOperation(_value) returns (uint256 _result) { return (true, _result); } catch { // Wait before retry (in actual applications may need more complex logic) if (i == _maxAttempts - 1) { return (false, 0); } } } return (false, 0); } // External call with timeout check function callWithTimeout( address _target, uint256 _value, uint256 _deadline ) external returns (bool success, uint256 result) { require(block.timestamp < _deadline, "Deadline passed"); try IExternalContract(_target).riskyOperation(_value) returns (uint256 _result) { return (true, _result); } catch { return (false, 0); } } }
7. Error Handling Best Practices
soliditycontract ErrorHandlingBestPractices { /* Best practices summary: 1. require use cases: - Validate function input parameters - Check external conditions - Validate return values 2. assert use cases: - Check internal invariants - Verify mathematical operation results - Check situations that shouldn't happen 3. revert use cases: - Complex condition judgments - Need detailed error information - Gas optimization (custom errors) 4. try/catch use cases: - External contract calls - Need graceful failure handling - Batch operations */ // Error definitions error InvalidInput(string param, string reason); error StateInvariantViolation(string invariant); error ExternalDependencyFailed(address dependency); uint256 public constant MAX_VALUE = 10000; uint256 public constant MIN_VALUE = 1; uint256 public totalValue; mapping(address => uint256) public values; // Comprehensive example function complexOperation( address _externalContract, uint256 _input, address _recipient ) external { // 1. Input validation (require) require(_externalContract != address(0), "Invalid contract"); require(_recipient != address(0), "Invalid recipient"); // Use custom error for detailed validation if (_input < MIN_VALUE || _input > MAX_VALUE) { revert InvalidInput( "_input", "Must be between MIN_VALUE and MAX_VALUE" ); } // 2. External call (try/catch) uint256 externalResult; try IExternal(_externalContract).getValue() returns (uint256 value) { externalResult = value; } catch Error(string memory reason) { revert ExternalDependencyFailed(_externalContract); } catch { revert ExternalDependencyFailed(_externalContract); } // 3. Calculation and state update uint256 oldTotal = totalValue; uint256 newValue = _input + externalResult; values[_recipient] += newValue; totalValue += newValue; // 4. Invariant check (assert) assert(totalValue >= values[_recipient]); assert(totalValue == oldTotal + newValue); assert(values[_recipient] >= newValue); } } interface IExternal { function getValue() external view returns (uint256); }
8. Error Handling Pattern Comparison
soliditycontract ErrorComparison { /* Comparison of three error handling methods: | Feature | require | assert | revert | |---------|---------|--------|--------| | Use case | Input validation | Internal invariants | Complex conditions | | Gas refund | Yes | No | Yes | | Error message | String | None | Custom error | | Gas consumption | Medium | High | Low (custom error) | | Severity | General | Serious (bug) | General | Recommended use cases: - Frequently called functions: Use custom errors - Input validation: Use require - Internal checks: Use assert - External calls: Use try/catch */ // Example: Choose error handling based on different scenarios // Scenario 1: Input validation - Use require function deposit(uint256 _amount) external { require(_amount > 0, "Amount must be positive"); // ... } // Scenario 2: Gas optimization - Use custom error error InvalidAmount(uint256 provided, uint256 minimum); function optimizedDeposit(uint256 _amount) external { if (_amount < 100) { revert InvalidAmount(_amount, 100); } // ... } // Scenario 3: Internal invariant - Use assert function internalOperation() external { uint256 before = totalSupply; // ... complex operations assert(totalSupply >= before); // Should not decrease } uint256 public totalSupply; }
9. Error Handling in Real Projects
solidity// OpenZeppelin style error handling contract ProductionGradeErrors { // Define modular errors // General errors error ZeroAddress(); error ZeroAmount(); error ExceedsMax(uint256 provided, uint256 max); error InsufficientBalance(uint256 available, uint256 required); error Unauthorized(address caller); error AlreadyInitialized(); error NotInitialized(); // Feature-specific errors error TransferFailed(address from, address to, uint256 amount); error SwapFailed(address tokenIn, address tokenOut, uint256 amount); error StakingPeriodNotEnded(uint256 endTime, uint256 currentTime); error CooldownPeriodActive(uint256 cooldownEnd); // State variables bool private _initialized; address public owner; mapping(address => uint256) public balances; mapping(address => uint256) public stakingEndTime; uint256 public constant COOLDOWN_PERIOD = 7 days; modifier onlyOwner() { if (msg.sender != owner) revert Unauthorized(msg.sender); _; } modifier initializer() { if (_initialized) revert AlreadyInitialized(); _initialized = true; _; } // Initialize function function initialize(address _owner) external initializer { if (_owner == address(0)) revert ZeroAddress(); owner = _owner; } // Deposit function function deposit(uint256 _amount) external { if (_amount == 0) revert ZeroAmount(); balances[msg.sender] += _amount; } // Withdraw function function withdraw(uint256 _amount) external { uint256 balance = balances[msg.sender]; if (balance < _amount) { revert InsufficientBalance(balance, _amount); } // Check cooldown period uint256 cooldownEnd = stakingEndTime[msg.sender] + COOLDOWN_PERIOD; if (block.timestamp < cooldownEnd) { revert CooldownPeriodActive(cooldownEnd); } balances[msg.sender] = balance - _amount; (bool success, ) = msg.sender.call{value: _amount}(""); if (!success) { revert TransferFailed(address(this), msg.sender, _amount); } } // Stake function function stake(uint256 _amount, uint256 _duration) external { if (_amount == 0) revert ZeroAmount(); uint256 balance = balances[msg.sender]; if (balance < _amount) { revert InsufficientBalance(balance, _amount); } uint256 maxDuration = 365 days; if (_duration > maxDuration) { revert ExceedsMax(_duration, maxDuration); } balances[msg.sender] = balance - _amount; stakingEndTime[msg.sender] = block.timestamp + _duration; } // Unstake function function unstake() external { uint256 endTime = stakingEndTime[msg.sender]; if (endTime == 0) revert NotInitialized(); if (block.timestamp < endTime) { revert StakingPeriodNotEnded(endTime, block.timestamp); } // Execute unstake... } }
10. Summary
Solidity's error handling mechanisms each have their characteristics:
-
require:
- Used for input validation and external condition checking
- Provides clear error messages
- Refunds remaining Gas
-
assert:
- Used for internal invariant checking
- Failure indicates a bug in the code
- Consumes all Gas (serious error)
-
revert + custom errors:
- Recommended since Solidity 0.8.4
- Significantly saves Gas
- Supports parameterized error messages
-
try/catch:
- Handles external call exceptions
- Supports graceful degradation
- Essential for batch operations
-
Best practices:
- Use custom errors for frequently called functions
- Use require for input validation
- Use assert for internal checks
- Use try/catch for external calls
- Design a clear, modular error system