Error Handling
When a call reverts, all state changes in that call frame are undone, and the error data is passed to the caller.
17.1 Reverting
When a call reverts, all state changes in that call frame are undone, and the error data is passed to the caller. Remaining gas is returned (except for Panic from assert before 0.8.0, which consumed all gas via INVALID).
require(cond, "msg") input / state validation require(cond, CustomError(args)) same, with custom error (0.8.26+) revert from complex logic revert CustomError(args) preferred: cheaper, structured custom error selector + args invariants that should never fail (bugs) overflow, div by 0, out-of-bounds, etc.
17.2 Custom errors
Custom errors are encoded like function calls: 4-byte selector of InsufficientBalance(uint256,uint256) + args. They save deployment gas (no long strings in bytecode) and runtime gas.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
error InsufficientBalance(uint256 available, uint256 required);
contract Bank {
error ZeroAmount();
mapping(address => uint256) public balanceOf;
function deposit() external payable {
require(msg.value > 0, ZeroAmount()); // 0.8.26+ syntax
balanceOf[msg.sender] += msg.value;
}
function withdraw(uint256 amount) external {
uint256 bal = balanceOf[msg.sender];
if (amount > bal) revert InsufficientBalance(bal, amount);
balanceOf[msg.sender] = bal - amount;
(bool ok, ) = msg.sender.call{value: amount}("");
require(ok, "transfer failed");
}
function checkInvariant() external view {
assert(address(this).balance >= 0); // trivially true; assert is for invariants
}
}17.3 Panic codes
assert failed arithmetic overflow/underflow outside unchecked division or modulo by zero invalid enum conversion incorrectly encoded storage byte array .pop() on empty array array index out of bounds too much memory allocated call to a zero-initialized internal function variable
17.4 try / catch
try works only for external calls and contract creation (new).
Errors inside the try expression's *argument evaluation* or in the success block are not caught.
If the callee returns data that fails to decode, the caller reverts (not caught).
Calling a high-level function on an address without code makes the caller revert (the compiler inserts an extcodesize check), and that revert is not caught.
The callee may consume nearly all gas (the caller retains only 1/64 per EIP-150).
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
interface IOracle {
function price() external view returns (uint256);
}
contract Consumer {
event Failure(string reason);
event PanicCode(uint256 code);
event LowLevel(bytes data);
function safePrice(IOracle oracle) external returns (uint256) {
try oracle.price() returns (uint256 p) {
return p;
} catch Error(string memory reason) { // revert("...") / require(false, "...")
emit Failure(reason);
} catch Panic(uint256 code) { // assert, overflow, etc.
emit PanicCode(code);
} catch (bytes memory lowLevelData) { // custom errors and everything else
emit LowLevel(lowLevelData);
}
return 0;
}
}17.5 Decoding a custom error in the catch block
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
error InsufficientBalance(uint256 available, uint256 required);
interface IBank { function withdraw(uint256 amount) external; }
contract ErrorDecoder {
event Short(uint256 available, uint256 required);
function tryWithdraw(IBank bank, uint256 amount) external {
try bank.withdraw(amount) {
} catch (bytes memory data) {
if (data.length >= 4 && bytes4(data) == InsufficientBalance.selector) {
(uint256 available, uint256 required) = this.decodeArgs(data);
emit Short(available, required);
} else {
assembly { revert(add(data, 32), mload(data)) } // bubble up any other error
}
}
}
// Helper using calldata slicing to skip the 4-byte selector
function decodeArgs(bytes calldata data) external pure returns (uint256, uint256) {
return abi.decode(data[4:], (uint256, uint256));
}
}