require and revert: Why a Failed Call Leaves No Trace
Failure in Solidity is an undo, not an error. What require and revert actually do, why nothing partial ever happens, and what the caller pays when a call fails.
TL;DR
requirechecks a condition, and if it is false, stops everything and rewinds the call as if it never happened. Nothing partial is left behind.- Reverting undoes every state change made so far in the transaction, including changes made by other contracts it called.
- The caller still pays for the gas used up to the failure. The work was done, it just left no lasting trace.
revertis the same mechanism written directly, and custom errors are the modern, cheaper way to say why.assertis for conditions that should be impossible. Meeting one means the contract has a bug, not that the caller did something wrong.
What does require do in Solidity?
require(condition, "message") evaluates a condition, and if it is false, immediately stops execution and rewinds every state change made so far, returning the message to the caller. If it is true, execution continues as if the line were not there.
WETH9 uses one, and it is the only thing standing between what you put in and what you can take out:
function withdraw(uint wad) public {
require(balanceOf[msg.sender] >= wad);
balanceOf[msg.sender] -= wad;
msg.sender.transfer(wad);
emit Withdrawal(msg.sender, wad);
}
One rule, then the work. The three lines below it can only ever run if the condition was true, because a false one ends the call before execution reaches them.
Note what this require does not have: a message. In this older dialect that is common, and the call simply stops with nothing to say. Newer code attaches a string or, better, a custom error.
What actually happens when a call reverts?
Everything is undone. This is the part that makes Solidity unlike most languages you may have used.
Walk it through with a concrete case. Ben holds 1 WETH and tries to withdraw 3.
- The call enters the function.
- The
requirechecks whether his entry is at least 3. It is 1. The condition is false. - Execution stops right there. The three lines below it never run.
- Every state change made during this call is rewound.
- The transaction ends as a failure.
No ETH moves. No balance is left half updated. The chain looks exactly as it did before Ben tried, with one exception: the gas he spent on the attempt is gone.
All or nothing is the rule, and it holds across contracts too. If a transaction calls contract A, which calls B, which calls C, and C reverts, then the work A and B did is rewound as well. There is no partial success in a transaction unless the code went out of its way to catch the failure deliberately.
What does a failed call cost?
Gas, for the work actually performed before the failure, and nothing else.
This is a fair and often misunderstood trade. The network really did execute those instructions. Validators really did spend resources. That is paid for. What the caller does not lose is anything the transaction would have moved, because none of it moved.
So the honest summary of Ben's attempt is: he paid a small fee to be told no. His 1 WETH is still his, untouched, and the contract's ETH is exactly where it was. It did not fine him and did not keep anything. It followed its own text, and its text said stop.
Notice how little the contract had to do to be safe here. It did not compare Ben against a list, or consult an owner, or check a permission. It looked up one number and compared it. See state variables in Solidity for why that number is the whole story.
What is the difference between require, revert, and assert?
Three ways to stop, with different intents.
| Statement | Use for | Meaning when triggered |
|---|---|---|
require(cond, "msg") | Checking inputs and conditions | The caller asked for something not allowed |
revert CustomError() | The same, written directly | The same, with structured detail |
assert(cond) | Conditions that should be impossible | The contract itself has a bug |
require and revert are the same mechanism. require(x > 0, "too small") and if (x == 0) revert TooSmall(); do the same thing. The second form uses a custom error, declared like this:
error OwnableUnauthorizedAccount(address account);
function _checkOwner() internal view virtual {
if (owner() != _msgSender()) {
revert OwnableUnauthorizedAccount(_msgSender());
}
}
That is OpenZeppelin's Ownable, and it is the modern style for two reasons that matter to a reader. Custom errors are cheaper than string messages, because a string has to be stored in the contract's code. And they carry data, so the failure tells you not just that the caller was wrong but exactly which address was rejected. Put the two files side by side and you can watch the language grow up: a bare require with nothing to say, and a typed error naming the account.
assert is different in intent. It is for invariants the author believes can never be false. If one fails, the conclusion is not "the caller did something wrong" but "this contract does not work the way its author thought". When you see assert while reading, treat it as a claim about what the author considered impossible, which is often the most interesting sentence on the page.
Why do contracts check conditions first?
Because the contract cannot be interrupted, negotiated with, or appealed to. Whatever its text says, happens.
That leads to a distinctive shape you will see everywhere in Solidity: a block of checks at the top of a function, then the work. Check, check, check, then act. In our contract it is two lines of checks and one line of action.
Reading this shape is a fast way to understand what a function requires without understanding how it works. Take any function, read only its require lines, and you have its preconditions, in order, in the author's own words. It is the fastest shortcut inside how to read a smart contract. That is often enough to answer the question you opened the file with.
The one caution worth carrying: a check is only as good as the value it reads. require(balanceOf[msg.sender] >= wad) compares a stored entry against a requested amount, and that entry can change between one transaction and the next. What the line guarantees is that the condition held at the instant it ran. Where those instants matter, the analysis gets deeper than reading, and that is the territory of reentrancy and the rest of the security literature. This article stops at what the words do.
Delete the word and ask what breaks
- Delete the
requireand what leaves the contract is no longer tied to what anyone put in. That one line is the entire connection between the two. - Swap
>=for>and withdrawing your exact balance stops working. Off-by-one changes in a comparison are a whole genre of contract bug. - Add a message,
require(balanceOf[msg.sender] >= wad, "insufficient"), and behaviour is identical but a failed call can finally explain itself. - Replace it with
assertand the failure still stops the call, but the intent is now wrong: asking for more than you hold is an expected condition, not an impossible one.
Related questions
What is the difference between require and revert?
They are the same mechanism with different syntax. require takes a condition and stops if it is false. revert stops unconditionally where it appears, and is typically used inside an if. Modern code often prefers revert with a custom error because it is cheaper and can carry data.
Does a reverted transaction cost gas? Yes, for the work executed before the revert. The caller pays for the computation performed, but no state change survives and no value moves.
What happens to state changes when a call reverts? All of them are undone, including changes made by other contracts called during the same transaction. The chain ends up exactly as it was before, apart from the gas spent.
What is a custom error in Solidity?
A declared error type, for example error NotOwner(address caller), used with revert. It costs less than a string message because it is not stored as text, and it can carry parameters that describe the failure.
When should assert be used instead of require?
assert is for conditions the author believes cannot be false, so a failure signals a bug in the contract. require is for validating inputs and conditions that callers can legitimately fail to meet.
Can a contract catch a revert from another contract?
Yes, using try and catch around an external call, which lets the caller handle the failure instead of being rewound with it. Without that, a revert deeper in the call stack takes the whole transaction with it.
Where to go next
Failure in Solidity is not an error code that a caller can ignore. It is an undo, applied to everything the transaction touched, and it is why contracts can be written as a list of rules followed by an action. Read the rules, and you know what the function demands.
That is the last word in our 24-line contract. The next step is taking the method somewhere real: how to read a contract on Etherscan, where the code you are reading is deployed, holding money, and was not written for you.
Tagged