Solidity Error Handling: require, assert, and revert
Error handling lies at the core of writing secure and efficient smart contracts in Solidity. Whether you’re working on DeFi protocols, NFT marketplaces, or dApps, knowing how and when to use “require”, “assert”, and “revert” is crucial for preventing vulnerabilities and ensuring robust code execution.
Let’s see how these error-handling tools work, with a practical example of a contract checking which London football club deserves the title of "best" based on their UEFA Champions League (UCL) victories.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
contract BestClub {
mapping(string => bool) public hasWonUCL;
constructor() {
// Initialize clubs and their UCL status
hasWonUCL["Chelsea"] = true;
hasWonUCL["Arsenal"] = false;
hasWonUCL["Tottenham"] = false;
hasWonUCL["West Ham"] = false;
}
function checkBestClub(string memory club) public view returns (string memory) {
// Use require to validate UCL victory status
require(hasWonUCL[club], "Error: You must have won the UEFA Champions League!");
return string(abi.encodePacked(club, " is the best club in London!"));
}
function checkBestClubRevert(string memory club) public view returns (string memory) {
// Use revert for the same validation in a more complex conditional
if (!hasWonUCL[club]) {
revert("Error: You must have won the UEFA Champions League!");
}
return string(abi.encodePacked(club, " is the best club in London!"));
}
function internalInvariant() public pure {
// Use assert to ensure a critical internal condition
uint256 londonClubCount = 4; // Hypothetical number
assert(londonClubCount > 0); // This should always be true
}
// Custom error for additional context
error NotEligible(string clubName);
function customErrorCheck(string memory club) public view {
if (!hasWonUCL[club]) {
revert NotEligible(club);
}
}
}
Validating Inputs and States with “require”
The “require” function is your go-to for validating external inputs and ensuring that the contract state aligns with expected conditions. If a condition fails, “require” reverts the transaction and allows for an optional error message.
When to Use “require”
Input Checks: Ensure function arguments meet specific criteria.
State Checks: Validate contract state before execution.
External Call Checks: Confirm successful external calls.
Example: Checking UCL Status
function checkBestClub(string memory club) public view returns (string memory) {
require(hasWonUCL[club], "Error: You must have won the UEFA Champions League!");
return string(abi.encodePacked(club, " is the best club in London!"));
}
Here, “require” ensures only clubs with a UCL title can be declared the "best." If you pass "Arsenal", the function reverts with the error:
Error: You must have won the UEFA Champions League!
Why “require”? It’s ideal for catching user errors early, providing meaningful feedback, and refunding unused gas, making it cost-effective.
Catching Critical Bugs with “assert”
“assert” is stricter, designed to enforce internal consistency and catch logic errors that should never occur. It’s used to verify invariants within your contract.
When to Use “assert”
Invariant Enforcement: Ensure certain conditions always hold true.
Bug Detection: Catch critical issues in contract logic.
Example: Verifying Club Count
function internalInvariant() public pure {
uint256 londonClubCount = 4; // Hypothetical number
assert(londonClubCount > 0); // This should always be true
}
If londonClubCount were ever less than or equal to zero, “assert” would fail, signaling a severe bug and consuming all remaining gas.
Why “assert”? Use it sparingly for logic that must not fail. When “assert” triggers, it indicates a fundamental issue requiring immediate attention.
Handling Complex Errors with “revert”
“revert” provides a more flexible way to handle errors, allowing for dynamic error messages and complex conditions. It explicitly halts execution and reverts all state changes.
When to Use “revert”
Dynamic Validations: Handle multiple conditions with custom logic.
Custom Error Messages: Use detailed, contextual errors.
Fallback Scenarios: Provide a safety net for unexpected behavior.
Example: Validating Club Eligibility with “revert”
function checkBestClubRevert(string memory club) public view returns (string memory) {
if (!hasWonUCL[club]) {
revert("Error: You must have won the UEFA Champions League!");
}
return string(abi.encodePacked(club, " is the best club in London!"));
}
For optimized error handling, Solidity allows custom errors:
error NotEligible(string clubName);
function customErrorCheck(string memory club) public view {
if (!hasWonUCL[club]) {
revert NotEligible(club);
}
}
Why “revert”? It offers flexibility for complex error handling and is gas-efficient when paired with custom error types.
Comparing Solidity Error Handling Tools
Here’s a quick comparison to help you choose the right tool for the job:
Function | Purpose | Gas Refund | Error Message | Best Use Case |
require | Validate external conditions | Refunds unused gas | Optional custom message | Input validation, state checks |
assert | Enforce internal invariants | Consumes all gas | No message | Critical logic checks |
revert | Dynamic error handling | Refunds unused gas | Optional custom message | Complex conditions, fallbacks |
Custom Errors | Contextual error handling | Refunds unused gas | Defined in error type | Detailed, gas-efficient errors |
Conclusion
Understanding and implementing effective error handling is crucial for building secure and user-friendly smart contracts. By mastering “require”, “assert”, and “revert”, you can ensure your contracts function reliably and protect against potential vulnerabilities.
Let’s keep the conversation going! Connect with me on Twitter, or LinkedIn.
Subscribe to my newsletter
Read articles from Joshua Obafemi directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Joshua Obafemi
Joshua Obafemi
Software Developer || Web3 Advocate