Ethereum State Transitions: What Actually Happens When a Transaction Reverts Mid-Execution

You submit a transaction with a 200,000 gas limit, watch it get picked up, and then your wallet shows the dreaded red badge: failed. The state looks untouched. Your ETH is back where it started. You breathe out.

Then you notice the gas fee is still gone.

That asymmetry is the whole story. Everything else is plumbing.

The EVM Runs on a Snapshot, Not on Live State

The Ethereum Virtual Machine doesn't write directly to the global state database as it goes. It maintains a working copy, sometimes called a state journal, and only commits that copy to canonical state if execution completes successfully. Think of it like a database transaction with a ROLLBACK command held in reserve the entire time, except the plumber still charges you for the hours he worked before discovering the pipe was the wrong size.

When your transaction starts, the EVM notes the current state root. Every SSTORE opcode, every balance change, every contract deployment gets logged into that working copy. If execution hits a REVERT opcode, runs out of gas, or throws an invalid opcode, the EVM discards the working copy entirely. The canonical state looks exactly as it did before your transaction started.

Clean. Tidy. Now here's the part people get wrong.

Gas Is Gone. Always.

Gas is consumed by computation, not by successful state change. By the time the EVM discovers it needs to revert, it has already performed real work: fetching storage, hashing data, executing arithmetic. That work happened. You paid for it.

On a full out-of-gas failure, every unit you sent is consumed. On a REVERT opcode, the contract explicitly halts and returns unused gas to the caller, but whatever burned getting to that REVERT instruction is gone. A contract that runs 50,000 gas worth of computation before hitting a require() check that fails will refund you the remaining gas limit minus those 50,000 units. Not the whole amount.

The worked version: you submit with a 200,000 gas limit. The contract processes 80,000 gas of logic, then reverts. You get roughly 120,000 gas back. The 80,000 is spent, permanently, on work that produced no visible state change whatsoever.

The validator collected that fee. The network shrugged and moved on.

Nested Calls and the Bubble-Up Problem

A transaction isn't always a single execution context, and this is where things get genuinely subtle. Contract A can call Contract B, which calls Contract C. Each call creates a child execution context with its own gas allocation.

If Contract C reverts, that revert bubbles up to Contract B. Contract B can catch it (using a low-level `.call()` in Solidity, which returns a boolean rather than propagating the revert) or let it propagate. If Contract B catches the failure and continues executing, its own state changes can still commit. Contract C's state changes are wiped. Contract B's are not, as long as Contract B's execution ultimately succeeds.

Picture a DeFi router contract calling out to an AMM pool to swap tokens. The pool reverts because slippage exceeded the user's tolerance. The router catches that revert with a low-level call, logs the failure in its own storage, and returns gracefully. Transaction succeeds. Router's storage write commits. The pool's attempted state changes do not. Two contracts, one transaction, split outcomes.

This is exactly how certain aggregator contracts handle multi-route swaps: try each route, catch failures, keep moving. It's a sensible design pattern, and it only works because revert propagation is opt-out rather than mandatory.

What the Refund Mechanism Gets Wrong in People's Heads

The most persistent misconception is that a reverted transaction is essentially free because the state didn't change. It isn't, and believing otherwise will cost you. The validator collected fees for the computation performed up to the revert point. From the network's perspective, work happened. It just concluded with a rollback.

There's also a subtler point about SSTORE refunds. Ethereum has historically offered partial gas refunds for clearing storage slots (setting a non-zero value to zero), but those refunds are only granted if the transaction succeeds. A reverted transaction that would have cleared storage gets nothing. The gas was spent, the storage wasn't cleared, the refund pool never materializes.

So ask yourself: how many contracts in production are front-loading their validation? Not enough. The practical discipline here isn't about avoiding reverts at all costs. It's about knowing where your gas goes when they happen. A require() that fires on line two of a function is vastly cheaper than one that fires after 60,000 gas of computation. The EVM doesn't give you credit for failing gracefully late in the call stack.

Front-load your checks. Revert early, revert cheap.

The validator gets paid either way.