The Revert Model
Solidity error handling is built on the revert model: when an error occurs, all state changes made during the transaction are undone as if it never ran, and remaining gas is returned to the caller. This atomicity is a defining property of smart contracts — a transaction either fully succeeds or leaves the world untouched, so there is no such thing as a half-completed transfer. The three primary tools are require, revert, and assert, and choosing the right one communicates intent and affects gas behavior.
Cricket analogy: A revert is like a delivery called dead ball — everything that happened is wiped and the score resets to before the ball, so the batsmen gain nothing and lose nothing.
require: Validating Inputs and Conditions
require(condition, "message") checks a condition and, if it is false, reverts with an optional string reason, refunding remaining gas. It is the workhorse for validating function inputs, checking return values of external calls, and enforcing preconditions like sufficient balance or correct caller. Because a failing require refunds unused gas, it is appropriate for expected failure conditions caused by bad input — the caller simply made a mistake and gets their gas back. Before Solidity 0.8, require used the Error(string) mechanism; the string reason is ABI-encoded and stored, which costs gas proportional to its length.
Cricket analogy: A require is the umpire checking the batsman is ready before the bowler runs in — if not, the delivery is waved away and play simply resets, no penalty to anyone.
revert with Custom Errors
Since Solidity 0.8.4, you can define custom errors with error InsufficientBalance(uint256 available, uint256 required); and trigger them with revert InsufficientBalance(bal, amount);. Custom errors are significantly cheaper than long require strings because only a 4-byte selector (plus any arguments) is encoded, rather than a full string. They also carry structured data that off-chain tools can decode, making failures both cheaper and more informative. The plain revert("message") form still exists and is equivalent to a failing require with that message, useful inside complex if branches where a boolean condition is awkward.
Cricket analogy: A custom error is like a standardized dismissal code (lbw, caught, bowled) flashed on the scoreboard — a compact signal everyone decodes, versus writing a long paragraph explaining each out.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract Vault {
mapping(address => uint256) public balance;
address public owner = msg.sender;
// Gas-efficient custom errors with structured data
error NotOwner(address caller);
error InsufficientBalance(uint256 available, uint256 required);
function withdraw(uint256 amount) external {
// require: input/precondition validation (refunds gas on failure)
require(amount > 0, "amount must be positive");
uint256 bal = balance[msg.sender];
if (bal < amount) {
// revert with a custom error: cheaper than a long string
revert InsufficientBalance(bal, amount);
}
balance[msg.sender] = bal - amount;
// assert: an invariant that must never be violated
assert(balance[msg.sender] <= bal);
payable(msg.sender).transfer(amount);
}
function setOwner(address next) external {
if (msg.sender != owner) revert NotOwner(msg.sender);
owner = next;
}
}Custom errors (Solidity 0.8.4+) are the recommended default for revert conditions. They encode only a 4-byte selector plus arguments, saving both deployment bytecode (no long strings) and runtime gas, while giving off-chain callers structured, decodable failure data.
assert and Panic Errors
assert(condition) is reserved for checking internal invariants — conditions that should never be false if the code is correct, such as 'total supply never exceeds the cap after a valid mint'. A failing assert raises a Panic(uint256) error rather than an Error(string). Importantly, Solidity 0.8's automatic checks — integer overflow/underflow, division by zero, out-of-bounds array access — also trigger Panic errors. A well-written contract should almost never hit an assert; if it does in production, it signals a bug in the contract itself, not merely bad user input.
Cricket analogy: An assert is like the scoreboard's internal check that runs-scored never exceeds balls-times-six — if that ever fails, the scoring system itself is broken, not the players.
Do not use assert for input validation — before Solidity 0.8 a failing assert consumed all remaining gas, and it still signals a Panic (contract bug) rather than an expected user error. Use require or custom revert for anything a caller could legitimately trigger, and reserve assert for invariants that indicate a code defect if violated.
- Solidity uses the revert model: on error, all state changes roll back and remaining gas is refunded, giving atomic all-or-nothing transactions.
- Use require(condition, msg) to validate inputs and preconditions caused by bad user input.
- Custom errors (error X(...); revert X(...);) since 0.8.4 are the cheapest, most informative way to fail.
- revert("msg") is equivalent to a failing require and is handy inside complex if branches.
- assert is for internal invariants that should never fail; a violation signals a contract bug.
- Failing assert and Solidity 0.8's overflow/bounds checks raise Panic(uint256); require and revert raise Error(string) or custom errors.
- Never use assert for input validation — reserve it for conditions that indicate a code defect.
Practice what you learned
1. What happens to state changes and gas when a Solidity transaction reverts?
2. When should you prefer a custom error over require with a long string message?
3. Which error type does a failing assert() produce?
4. Why should assert NOT be used for validating user input?
5. What does the revert model guarantee about a token transfer that fails midway?
Was this page helpful?
You May Also Like
Conditionals and Loops in Solidity
Learn how Solidity handles branching with if/else and repetition with for, while, and do-while loops, and why unbounded loops are dangerous in a gas-metered environment.
Function Modifiers in Solidity
Function modifiers are reusable pieces of code that wrap function bodies to enforce preconditions like access control, using the special _ placeholder to mark where the function runs.
Functions and Visibility Modifiers
Understand how Solidity functions are declared and the four visibility levels — public, external, internal, and private — plus state mutability keywords like view and pure.
Events and Logging
Events emit indexed logs to the blockchain that off-chain applications can efficiently query, providing a cheap way to record activity that smart contracts themselves cannot read back.
Related Reading
Related Study Notes in Programming
Browse all study notesApache Spark Study Notes
Programming · 30 topics
ProgrammingApache Flink Study Notes
Programming · 30 topics
ProgrammingHadoop Study Notes
Programming · 30 topics
ProgrammingSnowflake Study Notes
Programming · 30 topics
ProgrammingApache Airflow Study Notes
Programming · 30 topics
Programmingdbt (Data Build Tool) Study Notes
Programming · 30 topics