Error Handling in Solidity: require, revert, assert, and Custom Errors
Error handling in Solidity lets you validate conditions and gracefully handle failures in your smart contracts. This guide explains the four main approaches: require for input validation, revert for complex conditions, assert for internal checks, and custom errors for gas efficiency.
Published on 20 October, 2025
Categories:
SolidityUnderstanding Error Functions
Solidity provides multiple ways to handle errors. Each serves a specific purpose and has different gas implications. Before Solidity 0.8.0, failed operations would silently continue or cause unpredictable behavior. Now, all these methods automatically revert the transaction and refund remaining gas.
require: The Input Validator
The require function is your first line of defense. Use it to validate user inputs, check conditions before execution, and verify external call results.
function withdraw(uint amount) public { require(balance[msg.sender] >= amount, "Insufficient balance"); balance[msg.sender] -= amount; }
When require fails, it reverts the transaction and returns the error message. All state changes get rolled back. The syntax is straightforward:
require(condition, "error message"). If the condition is false, execution stops immediately.revert: Explicit Control Flow
The revert statement gives you explicit control over when to stop execution. It's particularly useful inside complex if-else logic where require would be awkward.
function buyTicket(uint ticketId) public payable { if (msg.value < ticketPrice) { revert("Payment too low"); } if (tickets[ticketId].sold) { revert("Ticket already sold"); } tickets[ticketId].owner = msg.sender; tickets[ticketId].sold = true; }
You can also use revert without a message:
revert(). This saves a tiny bit of gas but provides no information about what went wrong.assert: The Internal Consistency Checker
The assert function checks for conditions that should never be false. Think of it as a sanity check for your contract's internal logic. If an assert fails, something is seriously broken.
function transfer(address to, uint amount) public { uint previousBalance = balance[msg.sender]; balance[msg.sender] -= amount; balance[to] += amount; assert(balance[msg.sender] + amount == previousBalance); }
Before Solidity 0.8.0, assert consumed all remaining gas when it failed. Now it behaves like require in terms of gas refunds, but the distinction remains important for code readability. Use assert only for invariants that verify your contract's internal state integrity.
Custom Errors: Gas Efficient Error Handling
Custom errors, introduced in Solidity 0.8.4, save gas by avoiding expensive string storage. Define them outside functions and use them with revert.
error InsufficientBalance(uint requested, uint available); error Unauthorized(address caller); contract Wallet { mapping(address => uint) public balance; address public owner; function withdraw(uint amount) public { if (msg.sender != owner) { revert Unauthorized(msg.sender); } if (balance[msg.sender] < amount) { revert InsufficientBalance(amount, balance[msg.sender]); } balance[msg.sender] -= amount; } }
Custom errors can include parameters, making debugging easier. When the error reverts, you get typed data instead of a generic string. This approach costs significantly less gas than string-based errors.
Choosing the Right Error Method
Here's when to use each approach:
Use require for validating inputs, checking preconditions, and verifying return values from external calls. It's the most common error handler.
require(msg.sender == owner, "Only owner can call"); require(amount > 0, "Amount must be positive"); require(token.transfer(to, amount), "Transfer failed");
Use revert when you have complex conditional logic or need to perform checks inside nested if statements.
if (condition1) { if (condition2) { revert("Complex condition failed"); } }
Use assert for conditions that test internal invariants. These should never fail under normal operation.
assert(totalSupply >= balance[account]); assert(this.balance >= reservedFunds);
Use custom errors in production contracts where gas optimization matters. They're especially valuable in frequently called functions.
error PriceTooLow(uint provided, uint required); if (msg.value < price) { revert PriceTooLow(msg.value, price); }
Practical Example: Complete Contract
Here's a realistic contract demonstrating all error types:
// SPDX-License-Identifier: MIT pragma solidity ^0.8.4; error NotOwner(); error InsufficientFunds(uint available, uint required); contract Vault { address public owner; mapping(address => uint) public balances; uint public totalDeposited; constructor() { owner = msg.sender; } function deposit() public payable { require(msg.value > 0, "Must deposit something"); balances[msg.sender] += msg.value; totalDeposited += msg.value; assert(address(this).balance == totalDeposited); } function withdraw(uint amount) public { if (balances[msg.sender] < amount) { revert InsufficientFunds(balances[msg.sender], amount); } balances[msg.sender] -= amount; totalDeposited -= amount; (bool success, ) = msg.sender.call{value: amount}(""); require(success, "Transfer failed"); assert(address(this).balance == totalDeposited); } function emergencyWithdraw() public { if (msg.sender != owner) { revert NotOwner(); } uint contractBalance = address(this).balance; totalDeposited = 0; (bool success, ) = owner.call{value: contractBalance}(""); require(success, "Emergency withdrawal failed"); } }
Gas Costs Comparison
Gas consumption varies significantly between error types. Custom errors cost about 2000 gas less than require with a string message. For a function called thousands of times, this adds up.
// Costs more gas require(amount > 0, "Amount must be greater than zero"); // Costs less gas error InvalidAmount(); if (amount == 0) { revert InvalidAmount(); }
The difference comes from how Solidity stores strings versus error selectors. Strings get stored in contract bytecode, while custom errors use a 4-byte selector.
Error handling in Solidity protects your contracts from invalid states and provides clear feedback when operations fail. Use require for validation, revert for control flow, assert for invariants, and custom errors when gas efficiency matters. Each tool has its place, and choosing correctly makes your contracts safer and cheaper to use.