Part III · Chapter 10 of 43 · Byzantium

EIP-140: REVERT — failing cheaply, and saying why

Before Byzantium, a contract that hit a bad condition had one blunt option: throw — which undid its work but also torched every drop of the caller's remaining gas and returned no explanation. REVERT keeps the undo, gives the gas back, and lets the contract return an error. Every require("reason"), custom error, and try/catch you've ever written descends from it.

Updated Jun 24, 2026 · 10 min
Assumed
  • the EVM, gas & the call model
  • state changes

Byzantium, October 2017. A big fork — it brought the cryptographic precompiles that made on-chain zero-knowledge proofs possible and a call type that can’t touch state. But the change that every Solidity developer feels daily is smaller and more humble: a new way to fail. Up to here, a contract that detected a bad condition — an overdrawn balance, a failed permission check — could only throw, and throwing was a scorched-earth act: it undid the work, but it also burned every unit of gas the caller had left and handed back not one byte of explanation. EIP-140 introduces REVERT, and with it, failure becomes cheap, precise, and legible. Let’s derive it.

1 step Failure was scorched earth

The problem: aborting cost everything and said nothing

A contract call either succeeds and commits its state changes, or it aborts and they must be undone — you never want a half-applied transfer. Before Byzantium the only way to abort was to throw The pre-Byzantium way to abort: execute an invalid instruction (or Solidity's throw), which reverted the frame's state changes but consumed all remaining gas and returned no data. — typically by hitting an invalid opcode. That did undo the state, which is correct. But it came with two brutal side effects: it consumed all the caller’s remaining gas, and it returned no data at all. So a perfectly reasonable require(balance >= amount) that simply wasn’t met would burn the user’s entire gas budget and leave them staring at a bare “transaction failed” with no reason.

before: a failed check threw — expensive and silent your transaction require(bal ≥ amt) throw / invalid opcode state undone — but… ✗ all remaining gas burned · ✗ no reason returned a legitimate failed condition cost you everything and told you nothing
Before REVERT, a failed check threw: the frame's state was correctly undone, but every unit of remaining gas was burned and no error data came back. A legitimate, expected failure cost the user their whole gas budget and told them nothing about what went wrong.

→ Step 2: keep the undo, drop the punishment, add a reason.

2 step Undo, refund, explain

REVERT: three fixes in one opcode

REVERT The Byzantium opcode 0xfd. It halts the current call, reverts all of that frame's state changes atomically, returns the unused gas to the caller, and returns a data blob (offset, length) — used to carry an error reason. (opcode 0xfd) does exactly what we asked for, three things at once. It reverts all state changes made in the current call frame — atomically, as if the frame never ran. It refunds the remaining gas instead of burning it, so an expected failure costs only the gas actually used up to that point. And it returns data — an arbitrary (offset, length) blob from memory — which is where an error string or a structured error code rides back out.

REVERT (0xfd) — three things at once REVERT ① state — undone slot = 9 slot = 14 ✓ ② gas — kept 100% refunded ③ reason — returned "InsufficientBal" undo the frame · give the gas back · hand back an error — unlike a raw throw
REVERT does three things atomically: ① rolls back every state change in the current frame, ② refunds the unused gas to the caller instead of burning it, and ③ returns a data blob — so the contract can hand back a reason like “InsufficientBalance” rather than failing silently.
Formula
REVERT = undo(state) 1 + refund(gas) 2 + return(data) 3
  1. 1 every SSTORE, balance change, and log in this frame is reverted — atomic
  2. 2 the unused gas goes back to the caller; only gas already spent is lost
  3. 3 a memory blob bubbles up — the error string or custom-error data
The old throw did only the first, and at maximum cost. REVERT keeps the atomic undo but adds the refund and the reason.

→ Step 3: let the caller catch it.

3 step Catchable by the caller

The payoff: a failure the caller can catch and read

Because REVERT returns data and doesn’t nuke the transaction wholesale, a sub-call that reverts comes back to its caller as an ordinary failed call carrying its reason — not a chain-killing detonation. The callee’s own state changes are undone, control returns to the caller, and the caller gets to decide: propagate the error up, or handle it. That is precisely the machinery behind the error-handling you write every day.

a sub-call can fail without killing the caller caller try — call catch (reason) ✓ call revert("reason") ↩ callee REVERT + reason its state undone the revert returns as a failed call — the caller decides what to do this is what require(reason), custom errors, and try/catch are built on
When a callee REVERTs, the failure returns to the caller as a catchable result with its reason attached — the callee's state is undone, but the caller keeps running and chooses what to do. This is exactly what require(reason), custom errors, and Solidity's try/catch are built on.
04 Go Deeper Where to take it from here