Solidity Cheatsheet

Error Handling

Use this Solidity reference while you build software engineering projects, review code for technical interview prep, or polish examples for a software engineer resume.

Three Mechanisms

MechanismGas refundABI-encodedRecommended use
revert(string)yes (unused gas)string messagelegacy
require(cond, string)yesstring messageinput validation
assert(cond)no (pre-0.8) / yes (0.8+)panic codeinvariants
revert CustomError(...)yescustom ABImodern (cheapest)

require

// Basic
require(amount > 0, "Amount must be positive");
require(msg.sender == owner, "Not owner");
require(balances[msg.sender] >= amount, "Insufficient balance");

// Without message (saves gas — no string in bytecode)
require(amount > 0);

// With complex condition
require(
    block.timestamp >= startTime && block.timestamp <= endTime,
    "Sale not active"
);
  • Refunds remaining gas to caller.
  • Typically used for: input validation, access control, preconditions.
  • Using require without a string saves ~200 gas (no string in bytecode).

revert

// With string (legacy)
revert("Something went wrong");

// With custom error (preferred since 0.8.4)
error Unauthorized(address caller, bytes32 role);
error InsufficientBalance(uint256 requested, uint256 available);

function withdraw(uint256 amount) external {
    if (msg.sender != owner) revert Unauthorized(msg.sender, ADMIN_ROLE);
    if (balances[msg.sender] < amount)
        revert InsufficientBalance(amount, balances[msg.sender]);
    balances[msg.sender] -= amount;
    payable(msg.sender).transfer(amount);
}

Custom Errors (0.8.4+)

// Declared at file level or inside contract
error ZeroAddress();
error InvalidAmount(uint256 got, uint256 min, uint256 max);
error TransferFailed(address token, address from, address to);

contract Vault {
    function deposit(uint256 amount) external {
        if (amount == 0) revert ZeroAddress(); // smallest: 4 bytes on wire
        // ...
    }
}

Why custom errors: - Encoded as 4-byte selector + ABI-encoded args — much cheaper than strings. - Readable in ethers.js via error.errorName / decoded args. - Can carry structured data for debugging.

// Decoding in ethers.js
try {
    await contract.withdraw(amount);
} catch (e) {
    if (e.errorName === "InsufficientBalance") {
        console.log(e.errorArgs.requested, e.errorArgs.available);
    }
}

assert

// For internal invariants that should NEVER fail
function transfer(address to, uint256 amount) external {
    uint256 totalBefore = totalSupply;
    balances[from] -= amount;
    balances[to]   += amount;
    assert(totalSupply == totalBefore); // invariant check
}
  • In 0.8+: assert uses REVERT opcode (like require) — refunds unused gas. Previously used INVALID (consumed all gas).
  • Produces Panic error code, not a string.
  • Failing assert indicates a bug in your code, not a user error.

Panic Codes

CodeCause
0x00Generic panic (assert in < 0.8)
0x01assert(false)
0x11Arithmetic overflow/underflow
0x12Division or modulo by zero
0x21Invalid enum value conversion
0x22Incorrect storage byte array encoding
0x31pop() on empty array
0x32Index out of bounds
0x41Too much memory allocated
0x51Call to a zero-initialized function variable

try / catch

Used for external function calls and contract creation. Cannot catch internal call failures.

interface IOracle {
    function getPrice() external view returns (uint256);
}

contract Consumer {
    function safeGetPrice(address oracle) external view returns (uint256 price, bool ok) {
        try IOracle(oracle).getPrice() returns (uint256 p) {
            return (p, true);
        } catch Error(string memory reason) {
            // revert("message") or require(false, "message")
            emit Failed(reason);
            return (0, false);
        } catch Panic(uint256 code) {
            // assert failure, overflow, etc.
            emit PanicCode(code);
            return (0, false);
        } catch (bytes memory lowLevelData) {
            // Custom error or unknown revert data
            emit Unknown(lowLevelData);
            return (0, false);
        }
    }
}

try with Contract Deployment

contract Factory {
    event Deployed(address addr);
    event DeployFailed(string reason);

    function deploy(bytes32 salt) external {
        try new MyContract{salt: salt}() returns (MyContract c) {
            emit Deployed(address(c));
        } catch Error(string memory reason) {
            emit DeployFailed(reason);
        }
    }
}

try Gotchas

  • Only works for external calls (including this.func()).
  • Does not catch failures inside the try success branch — those propagate normally.
  • If the called contract returns successfully but returns decode fails, it goes to the low-level catch.

Overflow and Underflow (0.8+)

uint256 x = type(uint256).max;
x += 1; // REVERTS with Panic(0x11) in 0.8+

// Opt out for gas savings when you know it's safe
unchecked {
    uint256 wrapped = x + 1; // 0, no revert
    for (uint i; i < 100; ++i) { } // unchecked loop counter saves ~50 gas/iter
}

Use unchecked only when you have mathematically proven no overflow is possible.

Safe Math Pattern (Pre-0.8 / Libraries)

// No longer needed in 0.8+ for basic arithmetic
// OpenZeppelin SafeMath is deprecated for 0.8+ contracts
// Only use unchecked when you need the old wrapping behavior

Graceful Degradation Pattern

// Instead of reverting, return a sentinel value
function tryGetPrice(address feed) internal view returns (uint256) {
    try AggregatorV3Interface(feed).latestRoundData()
        returns (uint80, int256 price, uint256, uint256 updatedAt, uint80)
    {
        if (price <= 0) return 0;
        if (block.timestamp - updatedAt > 1 hours) return 0; // stale
        return uint256(price);
    } catch {
        return 0;
    }
}

Custom Error Inheritance

// Errors can be defined in interfaces and inherited
interface IErrors {
    error Unauthorized(address caller);
    error InvalidInput(string field);
}

contract MyContract is IErrors {
    function restricted() external {
        if (msg.sender != owner) revert Unauthorized(msg.sender);
    }
}

Bubble Up vs Wrap Errors

// Bubble up — re-throw the original revert
(bool ok, bytes memory data) = target.call(callData);
if (!ok) {
    assembly { revert(add(data, 32), mload(data)) }
}

// Wrap — add context
(bool ok2, ) = target.call(callData);
require(ok2, "External call to target failed");