5. Error Handling
Learn how to define and use custom errors for better debugging and user experience.
Define Custom Errors
Source: error.rs
use anchor_lang::prelude::*;
#[error_code]
pub enum ErrorCode {
#[msg("Unauthorized access")]
Unauthorized,
#[msg("Invalid amount provided")]
InvalidAmount,
#[msg("Arithmetic overflow occurred")]
ArithmeticOverflow,
#[msg("Account is not rent exempt")]
NotRentExempt,
#[msg("Invalid state transition")]
InvalidStateTransition,
#[msg("CPI call failed")]
CpiFailed,
#[msg("Invalid mint address")]
InvalidMint,
#[msg("Invalid token account")]
InvalidTokenAccount,
#[msg("Insufficient balance")]
InsufficientBalance,
#[msg("Program is paused")]
ProgramPaused,
#[msg("Insufficient permissions for this action")]
InsufficientPermissions,
#[msg("Role already exists")]
RoleAlreadyExists,
#[msg("Role not found")]
RoleNotFound,
}
Error Code Numbers
Anchor assigns error codes automatically:
- First error =
6000 - Second error =
6001 - Third error =
6002 - etc.
// Unauthorized = 6000
// InvalidAmount = 6001
// ArithmeticOverflow = 6002
Using Errors in Programs
1. require! Macro
Most common pattern for validation:
use crate::error::ErrorCode;
pub fn deposit_to_treasury_handler(
ctx: Context<DepositToTreasury>,
amount: u64
) -> Result<()> {
let treasury = &mut ctx.accounts.treasury;
// Validate amount
require!(amount > 0, ErrorCode::InvalidAmount);
// Check circuit breaker
require!(
!treasury.circuit_breaker_active,
ErrorCode::ProgramPaused
);
// Check balance
require!(
ctx.accounts.depositor.lamports() >= amount,
ErrorCode::InsufficientBalance
);
// ... proceed with deposit logic
Ok(())
}
Syntax:
require!(<boolean_condition>, ErrorCode::YourError);
2. require_eq! / require_neq!
For equality checks:
// Check equality
require_eq!(
user_account.owner,
ctx.accounts.signer.key(),
ErrorCode::Unauthorized
);
// Check inequality
require_neq!(
amount,
0,
ErrorCode::InvalidAmount
);
3. require_keys_eq! / require_keys_neq!
For Pubkey comparisons:
// Check pubkeys match
require_keys_eq!(
config.admin,
ctx.accounts.signer.key(),
ErrorCode::Unauthorized
);
// Check pubkeys don't match
require_keys_neq!(
sender.key(),
recipient.key(),
ErrorCode::InvalidRecipient
);
4. require_gt! / require_gte!
For numeric comparisons:
// Greater than
require_gt!(amount, 0, ErrorCode::InvalidAmount);
// Greater than or equal
require_gte!(
user.balance,
withdrawal_amount,
ErrorCode::InsufficientBalance
);
Errors in Constraints
Use errors directly in account constraints:
#[derive(Accounts)]
pub struct UpdateConfig<'info> {
#[account(
mut,
seeds = [SEED_PROGRAM_CONFIG],
bump = config.bump,
has_one = admin @ ErrorCode::Unauthorized, // Custom error
constraint = !config.paused @ ErrorCode::ProgramPaused
)]
pub config: Account<'info, ProgramConfig>,
pub admin: Signer<'info>,
}
Safe Arithmetic with Errors
Prevent integer overflow/underflow:
pub fn update_balance_handler(
ctx: Context<UpdateBalance>,
amount: u64
) -> Result<()> {
let account = &mut ctx.accounts.user_account;
// ❌ Unsafe - can overflow
// account.balance = account.balance + amount;
// ✅ Safe - returns error on overflow
account.balance = account
.balance
.checked_add(amount)
.ok_or(ErrorCode::ArithmeticOverflow)?;
Ok(())
}
Safe Arithmetic Methods
// Addition
let result = a.checked_add(b).ok_or(ErrorCode::ArithmeticOverflow)?;
// Subtraction
let result = a.checked_sub(b).ok_or(ErrorCode::ArithmeticOverflow)?;
// Multiplication
let result = a.checked_mul(b).ok_or(ErrorCode::ArithmeticOverflow)?;
// Division
let result = a.checked_div(b).ok_or(ErrorCode::DivisionByZero)?;
Error Handling Patterns
Pattern 1: Early Return
pub fn process_payment_handler(
ctx: Context<ProcessPayment>,
amount: u64
) -> Result<()> {
// Validate all inputs first
require!(amount > 0, ErrorCode::InvalidAmount);
require!(!ctx.accounts.config.paused, ErrorCode::ProgramPaused);
require!(
ctx.accounts.payer.lamports() >= amount,
ErrorCode::InsufficientBalance
);
// All validations passed, proceed with logic
// ...
Ok(())
}
Pattern 2: Custom Validation Functions
impl UserAccount {
pub fn validate_permissions(&self, required_permission: u8) -> Result<()> {
require!(
self.has_permission(required_permission),
ErrorCode::InsufficientPermissions
);
Ok(())
}
}
pub fn restricted_action_handler(ctx: Context<RestrictedAction>) -> Result<()> {
// Use helper function
ctx.accounts.user_account.validate_permissions(MANAGE_TOKENS)?;
// Proceed if validation passed
// ...
Ok(())
}
Pattern 3: Result Propagation
pub fn complex_operation_handler(ctx: Context<ComplexOperation>) -> Result<()> {
// Call helper that might fail
validate_state(&ctx.accounts.config)?;
process_transaction(&ctx.accounts)?;
update_metrics(&mut ctx.accounts.metrics)?;
Ok(())
}
fn validate_state(config: &ProgramConfig) -> Result<()> {
require!(!config.paused, ErrorCode::ProgramPaused);
require!(config.is_initialized, ErrorCode::NotInitialized);
Ok(())
}
Client-Side Error Handling
TypeScript
import { AnchorError } from "@coral-xyz/anchor";
try {
await program.methods
.updateConfig(newFee)
.accounts({
programConfig: configPda,
admin: admin.publicKey,
})
.rpc();
console.log("Config updated successfully");
} catch (error) {
if (error instanceof AnchorError) {
console.log("Error code:", error.error.errorCode.code);
console.log("Error number:", error.error.errorCode.number);
console.log("Error message:", error.error.errorMessage);
// Handle specific errors
switch (error.error.errorCode.code) {
case "Unauthorized":
console.error("You don't have permission to update config");
break;
case "ProgramPaused":
console.error("Program is currently paused");
break;
case "InvalidAmount":
console.error("Invalid fee amount provided");
break;
default:
console.error("Unknown error:", error.error.errorMessage);
}
} else {
console.error("Non-Anchor error:", error);
}
}
Parse Error Logs
try {
await program.methods.myInstruction().rpc();
} catch (error) {
// Error logs contain detailed information
console.log("Error logs:", error.logs);
// Example log:
// Program log: AnchorError thrown in programs/my_program/src/lib.rs:42:5
// Program log: Error Code: InvalidAmount
// Program log: Error Message: Invalid amount provided
}
Best Practices
✅ Do
- Use descriptive error messages - Help users understand what went wrong
- Validate early - Check all inputs before modifying state
- Use safe arithmetic - Always use
checked_*methods - Specific errors - Create specific error codes for different failure cases
- Document errors - Explain when each error can occur
#[error_code]
pub enum ErrorCode {
/// Thrown when the signer is not the program admin
#[msg("Only the program admin can perform this action")]
Unauthorized,
/// Thrown when amount is zero or exceeds maximum
#[msg("Amount must be between 1 and 1,000,000")]
InvalidAmount,
}
❌ Don’t
- Generic errors - Don’t use one error for multiple cases
- Silent failures - Always return errors, never ignore them
- Panic in production - Use
Result<()>instead ofpanic!() - Unclear messages - “Error” is not helpful
// ❌ Bad
#[msg("Error")]
GeneralError,
// ✅ Good
#[msg("Treasury balance insufficient for withdrawal")]
InsufficientTreasuryBalance,
Common Error Patterns
Ownership Validation
require_keys_eq!(
user_account.owner,
signer.key(),
ErrorCode::NotAccountOwner
);
Balance Checks
require_gte!(
user.balance,
amount,
ErrorCode::InsufficientBalance
);
State Validation
require!(
!program.is_paused && program.is_initialized,
ErrorCode::InvalidProgramState
);
Time-based Validation
let clock = Clock::get()?;
require!(
clock.unix_timestamp >= unlock_time,
ErrorCode::StillLocked
);
Permission Checks
require!(
role.has_permission(MANAGE_TREASURY),
ErrorCode::InsufficientPermissions
);
Debugging Errors
Enable Detailed Logs
In Anchor.toml:
[provider]
cluster = "localnet"
wallet = "~/.config/solana/id.json"
[scripts]
test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts"
[programs.localnet]
my_program = "..."
[registry]
url = "https://api.apr.dev"
# Enable detailed error logs
[features]
seeds = true
resolution = true
View Transaction Logs
solana confirm -v <transaction_signature>
Test Specific Errors
it("Should fail with InvalidAmount", async () => {
try {
await program.methods
.deposit(new BN(0)) // Invalid amount
.accounts({...})
.rpc();
expect.fail("Should have thrown error");
} catch (error) {
expect(error.error.errorCode.code).to.equal("InvalidAmount");
}
});
Next: Events →