Cairo provides robust error handling mechanisms for smart contracts. When an error occurs during contract execution, the transaction is immediately reverted and all state changes are undone.
Basic Error Functions
Cairo offers two main functions for error handling:
1. assert
-
Used for condition validation (similar to Solidity’s
require
)
-
Stops execution if the condition is false
-
Supports two formats:
assert(condition, 'error message'); // Basic assertion
assert!(condition, "formatted error: {}", x); // Formatted string error
2. panic
-
Used for immediate execution halt (similar to Solidity’s
revert
)
-
Best for complex conditions or internal errors
-
Supports multiple formats:
panic_with_felt252('error message'); // Basic panic
panic!("formatted error: value={}", value); // Formatted string error
While Cairo provides assertion macros like assert_eq!
and assert_ne!
, these are only for testing. In contract code, always use the standard assert
function.
Simple Example
Here’s a basic example demonstrating both error handling approaches:
#[starknet::interface]
trait ISimpleErrors<TContractState> {
fn check_positive(self: @TContractState, value: u128) -> bool;
fn set_value(ref self: TContractState, value: u128);
}
#[starknet::contract]
mod SimpleErrors {
use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess};
#[storage]
struct Storage {
value: u128,
}
#[abi(embed_v0)]
impl SimpleErrors of super::ISimpleErrors<ContractState> {
fn check_positive(self: @ContractState, value: u128) -> bool {
// Using assert for validation
assert(value > 0, 'Value must be positive');
true
}
fn set_value(ref self: ContractState, value: u128) {
// Using panic for immediate halt
if value == 0 {
panic_with_felt252('Value cannot be zero');
}
self.value.write(value);
}
}
}
Custom Error Codes
For better organization and consistency, you can define error messages in a dedicated module:
mod Errors {
const INSUFFICIENT_BALANCE: felt252 = "Insufficient balance";
const INVALID_AMOUNT: felt252 = "Invalid amount";
const ACCESS_DENIED: felt252 = "Access denied";
}
#[starknet::interface]
trait ICustomErrors<TContractState> {
fn withdraw(ref self: TContractState, amount: u128);
}
#[starknet::contract]
mod CustomErrors {
use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess};
use super::Errors;
#[storage]
struct Storage {
balance: u128,
}
#[abi(embed_v0)]
impl CustomErrors of super::ICustomErrors<ContractState> {
fn withdraw(ref self: ContractState, amount: u128) {
// Using custom error constants
assert(amount > 0, Errors::INVALID_AMOUNT);
assert(self.balance.read() >= amount, Errors::INSUFFICIENT_BALANCE);
let new_balance = self.balance.read() - amount;
self.balance.write(new_balance);
}
}
}
Real-World Example: Vault Contract
Here’s a practical example showing error handling in a vault contract that manages deposits and withdrawals:
mod Errors {
const INSUFFICIENT_BALANCE: felt252 = "Insufficient balance";
const INVALID_AMOUNT: felt252 = "Invalid amount";
const ACCESS_DENIED: felt252 = "Access denied";
}
#[starknet::interface]
trait IVaultErrors<TContractState> {
fn deposit(ref self: TContractState, amount: u128);
fn withdraw(ref self: TContractState, amount: u128);
fn get_balance(self: @TContractState) -> u128;
}
#[starknet::contract]
mod VaultErrors {
use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess};
use super::Errors;
#[storage]
struct Storage {
balance: u128,
owner: ContractAddress,
}
#[constructor]
fn constructor(ref self: ContractState, owner: ContractAddress) {
self.owner.write(owner);
}
#[abi(embed_v0)]
impl VaultErrors of super::IVaultErrors<ContractState> {
fn deposit(ref self: ContractState, amount: u128) {
assert(amount > 0, Errors::INVALID_AMOUNT);
let current_balance = self.balance.read();
self.balance.write(current_balance + amount);
}
fn withdraw(ref self: ContractState, amount: u128) {
assert(amount > 0, Errors::INVALID_AMOUNT);
let current_balance = self.balance.read();
if current_balance < amount {
panic_with_felt252(Errors::INSUFFICIENT_BALANCE);
}
self.balance.write(current_balance - amount);
}
fn get_balance(self: @ContractState) -> u128 {
self.balance.read()
}
}
}
In this example:
- Custom errors are defined in a separate module
- The
withdraw
function demonstrates both assert
and panic
approaches
- Balance checks protect against underflow conditions