0xMowgli.

/ Writing · · Updated · 6 min read

assert vs require in Solidity: which one to use, and what each failure means

By Oussama Allamou · Solidity · Error Handling · Smart Contracts

Use require for conditions that depend on something outside your contract: a caller's input, the sender's permissions, the result of an external call. Use assert for conditions that must be true if your own code is correct. Both revert the transaction when they fail. What differs is the meaning: a failed require is a caller getting told no, and a failed assert is a bug in your contract.

Follow one withdraw call through both checks. Use the buttons, the arrow keys, or tap the diagram.

CALLERrequire(amount <= balance)CHECKS WHAT THE CALLER SENTbalance -= amountSTATE CHANGES · ETHER LEAVESassert(total == sumOfBalances)CHECKS YOUR OWN CODECALL SUCCEEDSwithdraw(amount)REVERT · Error(string)REVERT · Panic(0x01)STATE COMMITTED
Explainer · require and assert in one call
  1. Step 01A call comes in

    Someone calls withdraw(amount). You do not control who they are or what number they pass. Everything that arrives from outside is untrusted.

  2. Step 02require guards the door

    require checks things the caller controls: the amount, the sender, the state they need. It runs first, before the function changes anything.

  3. Step 03A failed require is normal

    If the condition is false the call reverts with Error(string) carrying your message, or with a custom error. Every state change is rolled back and the unused gas goes back to the caller. Nothing is wrong with your contract. The caller asked for something they cannot have.

  4. Step 04The body runs

    With the inputs accepted, the function does its work: it updates balances and sends the ether.

  5. Step 05assert checks your own math

    assert states something that must be true no matter what any caller does, such as the total still matching the sum of balances. When it holds, the call succeeds and the state is committed.

  6. Step 06A failed assert is a bug

    If an assert ever fails the call reverts with Panic(0x01). Since Solidity 0.8.0 the unused gas is refunded, before that it burned all of it. Either way the meaning is the same: your code reached a state you believed was impossible, and it needs a fix.

What does require do?

require(condition, "message") checks a condition and reverts if it is false. The revert carries your message as Error(string), so wallets and block explorers can show the caller why the call failed.

Put require at the top of a function, before any state changes, and use it for anything you do not control. The slippage floor on a swap is a typical one: amountOutMin is enforced by a single require.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract Vault {
    mapping(address => uint256) public balanceOf;
    uint256 public totalDeposits;

    function withdraw(uint256 amount) external {
        // The caller picks amount. Check it.
        require(amount > 0, "Amount is zero");
        require(amount <= balanceOf[msg.sender], "Not enough balance");

        balanceOf[msg.sender] -= amount;
        totalDeposits -= amount;

        (bool ok, ) = msg.sender.call{value: amount}("");
        // The result of an external call is also outside your control.
        require(ok, "Transfer failed");
    }
}

Custom errors do the same job with less gas, because the revert data is a four-byte selector plus arguments instead of a string: if (amount == 0) revert ZeroAmount();. Compiler versions from 0.8.26 also accept a custom error as the second argument of require.

What does assert do?

assert(condition) checks something that should hold no matter who calls the function or with what. These conditions are called invariants. In the vault above, one invariant is that the contract always holds at least as much ether as it owes:

    function withdraw(uint256 amount) external {
        require(amount <= balanceOf[msg.sender], "Not enough balance");

        balanceOf[msg.sender] -= amount;
        totalDeposits -= amount;

        (bool ok, ) = msg.sender.call{value: amount}("");
        require(ok, "Transfer failed");

        // If this is ever false, the accounting above is wrong.
        assert(address(this).balance >= totalDeposits);
    }

A failed assert reverts with Panic(uint256) and the code 0x01. It takes no message, because there is nothing useful to tell the caller. The message is for you.

A check that can never be false is not an invariant. assert(x >= 0) on a uint256 is always true and tests nothing.

What is the difference between assert and require?

requireassert
ChecksInputs, permissions, external resultsInvariants of your own code
A failure meansThe caller asked for something invalidThe contract has a bug
Revert dataError(string) or a custom errorPanic(0x01)
MessageYesNo
State changesRolled backRolled back
Unused gas since 0.8.0RefundedRefunded
Unused gas before 0.8.0RefundedConsumed
Expected in productionYes, routinelyNever

What changed with gas in Solidity 0.8.0?

Before 0.8.0, a failed assert compiled to the INVALID opcode, which consumes all the gas left in the call. From 0.8.0 on it uses the REVERT opcode, the same one require uses, and the unused gas goes back to the caller.

Switch between the two compiler eras to see what happens to the gas that was left when the check failed.

Before Solidity 0.8.0

require fails
REVERT opcode · Error(string)
assert fails
INVALID opcode · no revert data

Solidity 0.8.0 and later

require fails
REVERT opcode · Error(string)
assert fails
REVERT opcode · Panic(0x01)

Gas used before the failure Refunded to the caller Consumed

Explainer · Gas left when a check fails
  1. Case 01Before Solidity 0.8.0

    A failed require gave the unused gas back. A failed assert compiled to the INVALID opcode and took everything the caller had left. Bar lengths are illustrative.

  2. Case 02Solidity 0.8.0 and later

    Both use the REVERT opcode and both refund the unused gas. The only difference left is the revert data: Error(string) for require, Panic(0x01) for assert. Bar lengths are illustrative.

So in a modern contract the two cost the caller the same when they fail. The reason to keep them apart is no longer gas. It is the signal: the revert data tells anyone reading a failed transaction whether they are looking at a rejected input or a broken contract.

The same Panic error is what the compiler's own checks produce. Arithmetic overflow reverts with Panic(0x11), division by zero with Panic(0x12), an out-of-bounds array index with Panic(0x32).

When should I use each one?

  • Input from a caller: require, or a custom error.
  • Access control: require, or a custom error.
  • Return value of an external call: require.
  • A relationship between your own state variables that must always hold: assert.
  • The result of your own arithmetic, checked after the fact: assert.

A practical test: can a user make this condition false by calling the contract in a way you allow? If yes, it is a require. If the only way to make it false is a mistake in your code, it is an assert.

Asserts earn their place during testing. Fuzzers such as Foundry's report any input that reaches a failing assert, and the SMTChecker built into the Solidity compiler tries to prove that each assert can never fail.

What do the terms mean?

  • Revert: stop execution and undo every state change made in the current call.
  • Invariant: a condition that holds before and after every function call, whatever the inputs.
  • Error(string): the revert data produced by require with a message.
  • Panic(uint256): the revert data produced by assert and by compiler-inserted checks. The number says which check failed.
  • Custom error: an error declared with error Name(args); and thrown with revert Name(args);.

FAQ

Does assert still consume all gas?

Not since Solidity 0.8.0. It now reverts with a Panic error and refunds the unused gas, like require. Contracts compiled with older versions still burn all remaining gas on a failed assert.

Can assert have an error message?

No. assert takes a single condition. If you want a message for the caller, the condition is probably about their input and belongs in a require.

What is the difference between require and revert?

They produce the same kind of failure. require(condition, "message") is shorthand for checking the condition and reverting when it is false. A bare revert statement is useful when the logic does not fit in one condition, and it is how custom errors are thrown.

Should I leave assert statements in production code?

Yes, when they guard a real invariant. They cost a little gas on every call, and in exchange a bug becomes a reverted transaction instead of corrupted state. Remove only the ones that can never be false.

What is Panic(0x01)?

It is the error code for a failed assert. Other codes come from checks the compiler adds for you, such as 0x11 for arithmetic overflow and 0x12 for division by zero.

Further reading