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.
Our contract has two of them, back to back:
function smash() external {
require(msg.sender == owner, "only the owner");
require(address(this).balance >= goal, "goal not reached");
payable(owner).transfer(address(this).balance);
}
Rule one: only the owner. Rule two: only past the goal. Then the payout. The payout line can only ever run if both conditions were true, because a false one ends the call before execution reaches it.
That is the whole rulebook of this contract, written in two lines, engraved on the glass where anyone can read it.
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 dropped 1 ETH into the pig last week. Today he wants it back, and the goal is nowhere near reached, so he calls smash().
- The call enters the function.
- Line one checks whether
msg.senderequalsowner. Ben is not Ana. The condition is false. - Execution stops right there. Lines two and three never run.
- Every state change made during this call is rewound.
- The transaction ends as a failure, and the message "only the owner" comes back.
No ETH moves. No variable is left half updated. The chain looks exactly as it did before Ben tried, with one exception: the gas Ben 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 ETH is still in the pig, still counting toward the goal, untouched. The contract did not judge him, did not fine him, and did not keep anything. It followed its own text, and its text said stop.
Notice what the contract could not do even if it wanted to: pay Ben back individually. It has no record of who deposited what, as state variables in Solidity explains. The rule and the missing memory are two different limits, and reading carefully keeps them separate.
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 NotOwner(address caller);
function smash() external {
if (msg.sender != owner) revert NotOwner(msg.sender);
}
Custom errors are the modern style, for two reasons that matter to a reader. They are cheaper than string messages, because a string has to be stored in the contract's code. And they can carry data, so the failure tells you not just that the caller was wrong but which address was rejected.
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(address(this).balance >= goal) compares the contract's balance against a stored number, and both of those 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 first
requireand anyone at all can callsmashand empty the pig. One line is the entire difference between a private savings jar and a public one. - Delete the second
requireand the owner can empty the pig at any time, goal or no goal. The contract still works, it just stopped being a savings commitment. - Swap
>=for>in the second check and reaching the goal exactly is no longer enough. Off-by-one changes in a comparison are a whole genre of contract bug. - Replace both with
assertand the failures still stop the call, but the intent is now wrong: a caller without permission 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