Learning Objectives – after this chapter you will be able to:
  • Explain state-reverting exceptions and transaction atomicity.
  • Use require, revert and assert correctly.
  • Define and use custom errors to save gas.
  • Handle failures of external calls with try/catch.
How Does Error Handling Work in Solidity?
  • Solidity uses state-reverting exceptions. When an error occurs, all changes made to the state in the current call (and its sub-calls) are undone, and an error is returned to the caller.
  • This makes transactions atomic: a transaction either completes fully or has no effect on the state at all.
  • The gas used up to the point of failure is still paid; any remaining gas is refunded (for require, revert, custom errors and – since Solidity 0.8.0 – assert).
  • Errors raised in an external call normally “bubble up” and revert the caller too, unless the caller catches them with try/catch or uses a low-level call.
The Main Error-Handling Tools
  • require(condition, "message") – the gate check. It validates inputs and conditions before (or during) execution, e.g. caller permissions, amounts and states. If the condition is false the call reverts with an Error(string) containing the message. Solidity 0.8.26 also introduced require(condition, CustomError()) (in 0.8.26 only with the via-IR pipeline).
  • revert("message") or revert CustomError(...) – reverts unconditionally. It is useful inside complex if/else logic where a single require would be awkward.
  • assert(condition) – checks internal invariants – conditions that should never be false if the code is correct (e.g. a total that must always equal the sum of balances). A failing assert indicates a bug and produces a Panic(uint256) error (code 0x01). Since 0.8.0 it no longer consumes all remaining gas.
  • Custom errors (Solidity 0.8.4+) – declared with error InsufficientBalance(uint available, uint required); and used with revert. They are cheaper than long strings and can carry data.
Require vs Revert vs Assert
Featurerequirerevertassert
PurposeValidate inputs, permissions and conditionsAbort from complex branching logicCheck invariants that must never fail
Typical placeStart of a functionInside if/else blocksEnd of a function / after critical updates
Error typeError(string) or custom errorError(string) or custom errorPanic(uint256)
Meaning of failureInvalid input / not allowedInvalid input / not allowedA bug in the contract
Remaining gasRefundedRefundedRefunded (since 0.8.0)

Other Panic codes are raised automatically by the compiler’s checks, e.g. 0x11 arithmetic overflow, 0x12 division by zero, 0x21 invalid enum conversion, 0x31 pop on an empty array and 0x32 array index out of bounds.

try / catch

A contract can catch a failure of an external function call or a contract creation instead of reverting itself:

try other.doSomething(x) returns (uint result) {
    // success
} catch Error(string memory reason) {
    // revert("...") or require(false, "...")
} catch Panic(uint code) {
    // assert failure, overflow, division by zero ...
} catch (bytes memory lowLevelData) {
    // custom errors and anything else
}

try/catch works only with external calls (and new); it cannot catch errors from internal function calls.

Example : require, revert, assert, custom errors and try/catch
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract Bank {
    error InsufficientBalance(uint available, uint required);

    mapping(address => uint) public balances;
    uint public totalDeposits;

    function deposit() external payable {
        require(msg.value > 0, "Deposit must be greater than zero");   // input check
        balances[msg.sender] += msg.value;
        totalDeposits += msg.value;
    }

    function withdraw(uint amount) external {
        uint available = balances[msg.sender];
        if (amount > available) {
            revert InsufficientBalance(available, amount);   // custom error with data
        }
        balances[msg.sender] = available - amount;
        totalDeposits -= amount;
        assert(address(this).balance >= totalDeposits);     // invariant check

        (bool ok, ) = payable(msg.sender).call{value: amount}("");
        require(ok, "Transfer failed");
        assert(address(this).balance >= totalDeposits);     // must still hold after paying
    }
}

contract BankUser {
    event Failed(string reason);
    event FailedLowLevel(bytes data);

    function tryWithdraw(Bank bank, uint amount) external returns (bool) {
        try bank.withdraw(amount) {
            return true;
        } catch Error(string memory reason) {
            emit Failed(reason);
        } catch (bytes memory data) {
            emit FailedLowLevel(data);           // custom error InsufficientBalance lands here
        }
        return false;
    }
}
Key Takeaways
  • An error reverts every state change of the call; transactions are atomic.
  • require/revert = invalid input or permission (Error or custom error); assert = bug detection (Panic).
  • Custom errors are gas-efficient; try/catch handles failures of external calls.
Download | Print      E-Notes

The PDF E-Notes are the earlier printable version of this chapter. The notes on this page are the latest, revised version.

Video : Error Handling in Solidity
Resource File
Question / Answer (15 questions)

Answer : Solidity uses state-reverting exceptions: when an error occurs, all state changes made in the current call and its sub-calls are undone and an error is returned to the caller. Transactions are therefore atomic.

Answer : A transaction either completes entirely or has no effect on the state at all; partial changes are never kept.

Answer : require, revert and assert.

Answer : To validate inputs, permissions and conditions before or during execution, e.g. require(msg.value > 0, "Send Ether");. A false condition reverts with the given message.

Answer : When the decision to fail is inside more complex logic such as nested if/else blocks, or when reverting with a custom error, e.g. if (x > y) revert TooLarge(x);.

Answer : Neither is better in general: require(condition) uses a revert internally. Use require for simple guard checks and revert for complex branching or custom errors.

Answer : To check internal invariants – conditions that should never be false if the code is correct. A failing assert signals a bug and raises a Panic error with code 0x01.

Answer : No. Since 0.8.0, assert raises Panic(uint256) using the revert mechanism and the remaining gas is refunded. Before 0.8.0 it used an invalid opcode that consumed all remaining gas.

Answer : Custom errors (Solidity 0.8.4+) are declared with error, e.g. error InsufficientBalance(uint available, uint required);, and raised with revert. They cost less gas than long error strings and can carry useful data.

Answer : Error(string) is produced by require and revert with a message, indicating invalid input or conditions. Panic(uint256) is produced by assert and by compiler checks (overflow, division by zero, out-of-bounds) and indicates a bug.

Answer : 0x01 – assert failed; 0x11 – arithmetic overflow or underflow; 0x12 – division or modulus by zero (also 0x32 – array index out of bounds).

Answer : To handle failures of external function calls or contract creation without reverting the calling contract, with separate catch blocks for Error(string), Panic(uint) and low-level error data.

Answer : No. try/catch works only with external calls and contract creation with new.

Answer : No. Gas consumed up to the point of failure is still paid; only the unused remaining gas is refunded.

Answer : The final catch (bytes memory data) block, because custom errors are not Error(string) or Panic(uint); they are delivered as raw encoded error data.
MCQ Test : Check Your Understanding (5 questions)

Select an option and press Check Answer, or answer all questions and press Submit Test to see your score. Each answer comes with an explanation.

Q1. Which statement is best for validating user input at the start of a function?

Show Answer

Correct Answer : B. require
require is the standard input/condition check.

Q2. A failing assert produces which error type?

Show Answer

Correct Answer : A. Panic(uint256)
assert raises Panic with code 0x01.

Q3. When a transaction reverts, the state changes it made are:

Show Answer

Correct Answer : D. Undone completely
Reverts undo all state changes of the call.

Q4. Custom errors were introduced in Solidity version:

Show Answer

Correct Answer : C. 0.8.4
Custom errors arrived in 0.8.4.

Q5. try/catch in Solidity can be used with:

Show Answer

Correct Answer : A. External calls and contract creation
try/catch applies to external calls and new.

Bibliography/References

The notes, questions and MCQs on this page are original explanations written for this course. The works listed below were consulted and are cited for attribution and further reading. Product names, logos and trademarks belong to their respective owners. Short quotations, where used, are marked and attributed.

  1. Solidity Team. (n.d.). Expressions and Control Structures (if/else, loops, checked arithmetic, error handling, try/catch). Solidity Documentation. https://docs.soliditylang.org/en/latest/control-structures.html
  2. Solidity Team. (n.d.). Contracts (creating contracts, visibility and getters, function modifiers, constant and immutable, functions, events, custom errors, inheritance, abstract contracts, interfaces, libraries). Solidity Documentation. https://docs.soliditylang.org/en/latest/contracts.html
  3. Solidity Team. (n.d.). Security Considerations. Solidity Documentation. https://docs.soliditylang.org/en/latest/security-considerations.html
  4. Solidity Team. (n.d.). Solidity Blog (release announcements). https://soliditylang.org/blog/
  5. Modi, R. (2018). Solidity Programming Essentials: A beginner’s guide to build smart contracts for Ethereum and blockchain. Packt Publishing. ISBN 978-1-78883-138-3.