# Error Handling in Solana Programs This reference provides comprehensive coverage of error handling patterns for native Rust Solana program development, including custom error types, error propagation, and best practices. ## Table of Contents 1. [Error Handling Fundamentals](#error-handling-fundamentals) 2. [ProgramError](#programerror) 3. [Custom Error Types](#custom-error-types) 4. [Error Propagation](#error-propagation) 5. [Error Context and Logging](#error-context-and-logging) 6. [Client-Side Error Handling](#client-side-error-handling) 7. [Best Practices](#best-practices) --- ## Error Handling Fundamentals ### Why Error Handling Matters **In Solana programs, errors serve multiple purposes:** 1. **Security:** Prevent invalid state transitions 2. **User Experience:** Provide meaningful feedback 3. **Debugging:** Identify issues quickly 4. **Transaction Validation:** Fail fast when invariants are violated **Key Principle:** Errors should cause the entire transaction to fail and rollback, maintaining atomicity. ### The Result Type All Solana program instructions return `ProgramResult`: ```rust use solana_program::{ entrypoint::ProgramResult, program_error::ProgramError, }; pub type ProgramResult = Result<(), ProgramError>; // Success pub fn successful_operation() -> ProgramResult { Ok(()) } // Failure pub fn failed_operation() -> ProgramResult { Err(ProgramError::Custom(42)) } ``` **When an instruction returns `Err`:** - Transaction fails immediately - All state changes rollback - Error code returned to client - Transaction fee still charged (for processing cost) --- ## ProgramError ### The Built-in Error Type Solana provides `ProgramError` enum with common error variants: ```rust use solana_program::program_error::ProgramError; pub enum ProgramError { // Common errors Custom(u32), // Custom error code InvalidArgument, // Invalid instruction argument InvalidInstructionData, // Failed to deserialize instruction data InvalidAccountData, // Invalid account data AccountDataTooSmall, // Account data too small InsufficientFunds, // Not enough lamports IncorrectProgramId, // Wrong program ID MissingRequiredSignature, // Required signer missing AccountAlreadyInitialized, // Account already initialized UninitializedAccount, // Account not initialized NotEnoughAccountKeys, // Not enough accounts provided AccountBorrowFailed, // Failed to borrow account data MaxSeedLengthExceeded, // PDA seed too long InvalidSeeds, // Invalid PDA derivation BorshIoError(String), // Borsh serialization error AccountNotRentExempt, // Account not rent-exempt IllegalOwner, // Wrong account owner ArithmeticOverflow, // Arithmetic overflow // ... and more } ``` ### Common ProgramError Usage ```rust use solana_program::program_error::ProgramError; pub fn validate_inputs( amount: u64, max_amount: u64, ) -> ProgramResult { // InvalidArgument: Input doesn't meet requirements if amount == 0 { return Err(ProgramError::InvalidArgument); } // InsufficientFunds: Not enough balance if amount > max_amount { return Err(ProgramError::InsufficientFunds); } // ArithmeticOverflow: Math operation failed let _result = amount.checked_mul(2) .ok_or(ProgramError::ArithmeticOverflow)?; Ok(()) } ``` --- ## Custom Error Types ### Why Custom Errors? **Built-in `ProgramError` is generic.** Custom errors provide: - **Specific error codes** for different failure modes - **Better debugging** with descriptive messages - **Client clarity** - clients know exactly what went wrong - **Documentation** - errors serve as API documentation ### Defining Custom Errors Use the `thiserror` crate to define custom error enums: ```rust use solana_program::program_error::ProgramError; use thiserror::Error; #[derive(Error, Debug, Copy, Clone)] pub enum NoteError { #[error("You do not own this note")] Forbidden, #[error("Note text is too long")] InvalidLength, #[error("Rating must be between 1 and 5")] InvalidRating, #[error("Note title cannot be empty")] EmptyTitle, #[error("Maximum notes limit reached")] MaxNotesExceeded, } ``` **Attributes explained:** - `#[derive(Error)]` - Implements `std::error::Error` trait - `#[derive(Debug)]` - Allows `{:?}` formatting - `#[derive(Copy, Clone)]` - Makes errors copyable (recommended) - `#[error("...")]` - Error message string ### Converting to ProgramError Implement `From for ProgramError`: ```rust impl From for ProgramError { fn from(e: NoteError) -> Self { ProgramError::Custom(e as u32) } } ``` **How it works:** 1. Custom error is converted to `u32` (using `as u32` cast) 2. Wrapped in `ProgramError::Custom(u32)` 3. Returned to client as error code **Error code mapping:** ```rust NoteError::Forbidden → ProgramError::Custom(0) NoteError::InvalidLength → ProgramError::Custom(1) NoteError::InvalidRating → ProgramError::Custom(2) NoteError::EmptyTitle → ProgramError::Custom(3) NoteError::MaxNotesExceeded → ProgramError::Custom(4) ``` ### Using Custom Errors ```rust pub fn create_note( program_id: &Pubkey, accounts: &[AccountInfo], title: String, content: String, rating: u8, ) -> ProgramResult { // Validation with custom errors if title.is_empty() { return Err(NoteError::EmptyTitle.into()); } if content.len() > 1000 { return Err(NoteError::InvalidLength.into()); } if rating < 1 || rating > 5 { return Err(NoteError::InvalidRating.into()); } // Continue processing... Ok(()) } ``` **The `.into()` method** automatically converts `NoteError` to `ProgramError`. ### Advanced Custom Error Types **With additional context:** ```rust #[derive(Error, Debug)] pub enum GameError { #[error("Insufficient mana: have {current}, need {required}")] InsufficientMana { current: u32, required: u32 }, #[error("Invalid move: {0}")] InvalidMove(String), #[error("Player not found: {0}")] PlayerNotFound(String), } ``` **Note:** Errors with fields cannot derive `Copy`, only `Clone`. --- ## Error Propagation ### The `?` Operator The `?` operator is Rust's error propagation mechanism: ```rust pub fn complex_operation( accounts: &[AccountInfo], ) -> ProgramResult { // If validation fails, error is returned immediately validate_accounts(accounts)?; // If deserialization fails, error is propagated let data = AccountData::try_from_slice(&accounts[0].data.borrow())?; // If checked math fails, ArithmeticOverflow is returned let result = data.value.checked_add(100) .ok_or(ProgramError::ArithmeticOverflow)?; Ok(()) } ``` **What `?` does:** 1. If `Result` is `Ok(value)`, unwraps to `value` 2. If `Result` is `Err(e)`, converts `e` and returns early 3. Conversion happens via `From` trait ### Error Conversion Chain ```rust // Step 1: Borsh deserialization fails let data = MyData::try_from_slice(bytes)?; // Returns: Err(std::io::Error) // Step 2: ? operator converts via From trait // std::io::Error → ProgramError::BorshIoError // Step 3: Custom error conversion return Err(MyError::InvalidData.into()); // MyError → ProgramError::Custom(n) ``` ### Manual Error Handling ```rust // Without ? pub fn manual_error_handling( account: &AccountInfo, ) -> ProgramResult { match validate_account(account) { Ok(()) => { // Continue processing } Err(e) => { msg!("Validation failed: {:?}", e); return Err(e); } } Ok(()) } // With ? (equivalent) pub fn automatic_error_handling( account: &AccountInfo, ) -> ProgramResult { validate_account(account)?; Ok(()) } ``` ### Mapping Errors Transform one error type to another: ```rust pub fn map_errors( account: &AccountInfo, ) -> ProgramResult { // Map generic error to custom error let data = AccountData::try_from_slice(&account.data.borrow()) .map_err(|_| NoteError::InvalidLength)?; // Map to different ProgramError variant let value = data.amount.checked_add(100) .ok_or(ProgramError::ArithmeticOverflow)?; Ok(()) } ``` ### Combining Multiple Operations ```rust pub fn chain_operations( accounts: &[AccountInfo], ) -> ProgramResult { // All operations must succeed or transaction fails let account1 = validate_and_load_account(&accounts[0])?; let account2 = validate_and_load_account(&accounts[1])?; let combined = account1.value .checked_add(account2.value) .ok_or(ProgramError::ArithmeticOverflow)?; update_account(&accounts[2], combined)?; Ok(()) } ``` --- ## Error Context and Logging ### Adding Context with `msg!` Use `msg!` macro to log context before returning errors: ```rust use solana_program::msg; pub fn transfer_tokens( from: &AccountInfo, to: &AccountInfo, amount: u64, ) -> ProgramResult { if amount == 0 { msg!("Transfer amount cannot be zero"); return Err(ProgramError::InvalidArgument); } let from_balance = get_balance(from)?; if from_balance < amount { msg!("Insufficient balance: have {}, need {}", from_balance, amount); return Err(ProgramError::InsufficientFunds); } // Perform transfer... Ok(()) } ``` ### Logging Best Practices **✅ Good logging:** ```rust msg!("Invalid rating: got {}, expected 1-5", rating); msg!("PDA derivation failed: expected {}, got {}", expected, actual); msg!("Account {} not owned by program {}", account.key, program_id); ``` **❌ Poor logging:** ```rust msg!("Error"); // Not helpful msg!("Failed"); // What failed? // (no logging) // Can't debug issues ``` ### Conditional Logging ```rust pub fn debug_operation( account: &AccountInfo, debug_mode: bool, ) -> ProgramResult { if debug_mode { msg!("Processing account: {}", account.key); msg!("Owner: {}", account.owner); msg!("Lamports: {}", account.lamports()); } // Process... Ok(()) } ``` ### Error with Recovery ```rust pub fn try_with_fallback( accounts: &[AccountInfo], ) -> ProgramResult { // Try primary method match process_primary(accounts) { Ok(()) => { msg!("Primary method succeeded"); Ok(()) } Err(e) => { msg!("Primary method failed: {:?}, trying fallback", e); // Try fallback process_fallback(accounts).map_err(|fallback_err| { msg!("Fallback also failed: {:?}", fallback_err); fallback_err }) } } } ``` --- ## Client-Side Error Handling ### Error Code Interpretation **Client receives:** ```json { "error": { "InstructionError": [ 0, { "Custom": 2 } ] } } ``` **Decoding:** - Instruction index: `0` (first instruction) - Error type: `Custom` - Error code: `2` ### TypeScript Error Mapping ```typescript // Define error codes matching Rust enum enum NoteError { Forbidden = 0, InvalidLength = 1, InvalidRating = 2, EmptyTitle = 3, MaxNotesExceeded = 4, } // Error messages const NOTE_ERROR_MESSAGES = { [NoteError.Forbidden]: "You do not own this note", [NoteError.InvalidLength]: "Note text is too long", [NoteError.InvalidRating]: "Rating must be between 1 and 5", [NoteError.EmptyTitle]: "Note title cannot be empty", [NoteError.MaxNotesExceeded]: "Maximum notes limit reached", }; // Parse error function parseNoteError(error: any): string { if (error?.InstructionError) { const [_, instructionError] = error.InstructionError; if (instructionError?.Custom !== undefined) { const errorCode = instructionError.Custom; return NOTE_ERROR_MESSAGES[errorCode] || `Unknown error: ${errorCode}`; } } return "Transaction failed"; } // Usage try { await program.methods.createNote(title, content, rating).rpc(); } catch (error) { const message = parseNoteError(error); console.error(message); } ``` ### Anchor Error Handling **With Anchor framework:** ```typescript import { AnchorError } from "@coral-xyz/anchor"; try { await program.methods.createNote(title, content, rating).rpc(); } catch (error) { if (error instanceof AnchorError) { console.error("Error code:", error.error.errorCode.code); console.error("Error message:", error.error.errorMessage); console.error("Error number:", error.error.errorCode.number); } } ``` --- ## Best Practices ### 1. Fail Fast **Return errors immediately when validation fails:** ```rust // ✅ Good - fails fast pub fn validate_input(rating: u8) -> ProgramResult { if rating < 1 || rating > 5 { return Err(NoteError::InvalidRating.into()); } // Continue only if valid Ok(()) } // ❌ Bad - continues with invalid state pub fn validate_input_bad(rating: u8) -> ProgramResult { if rating >= 1 && rating <= 5 { // Valid branch } // Continues regardless! Ok(()) } ``` ### 2. Meaningful Error Messages ```rust // ✅ Good - specific and actionable #[error("Username must be 3-20 characters, got {0}")] InvalidUsernameLength(usize), #[error("Insufficient mana: need {required}, have {current}")] InsufficientMana { required: u32, current: u32 }, // ❌ Bad - vague #[error("Invalid input")] InvalidInput, #[error("Error")] GenericError, ``` ### 3. Organize Errors by Category ```rust #[derive(Error, Debug, Copy, Clone)] pub enum GameError { // Validation errors (0-99) #[error("Invalid player name")] InvalidPlayerName, #[error("Invalid move")] InvalidMove, // State errors (100-199) #[error("Game not started")] GameNotStarted, #[error("Game already finished")] GameFinished, // Resource errors (200-299) #[error("Insufficient gold")] InsufficientGold, #[error("Inventory full")] InventoryFull, } ``` ### 4. Consistent Error Handling Pattern ```rust pub fn standard_operation_pattern( program_id: &Pubkey, accounts: &[AccountInfo], params: Params, ) -> ProgramResult { // 1. Parse accounts let account_info_iter = &mut accounts.iter(); let user = next_account_info(account_info_iter)?; let data_account = next_account_info(account_info_iter)?; // 2. Validate signers if !user.is_signer { msg!("User must sign the transaction"); return Err(ProgramError::MissingRequiredSignature); } // 3. Validate ownership if data_account.owner != program_id { msg!("Data account not owned by program"); return Err(ProgramError::IllegalOwner); } // 4. Validate input parameters if params.amount == 0 { msg!("Amount cannot be zero"); return Err(ProgramError::InvalidArgument); } // 5. Load and validate account data let mut data = AccountData::try_from_slice(&data_account.data.borrow())?; if !data.is_initialized { msg!("Account not initialized"); return Err(ProgramError::UninitializedAccount); } // 6. Perform operation // ... Ok(()) } ``` ### 5. Document Error Codes ```rust /// Error codes for the Note program. /// /// | Code | Error | Description | /// |------|-------|-------------| /// | 0 | Forbidden | Caller does not own the note | /// | 1 | InvalidLength | Note text exceeds maximum length | /// | 2 | InvalidRating | Rating not in range 1-5 | /// | 3 | EmptyTitle | Note title is empty | /// | 4 | MaxNotesExceeded | User has reached note limit | #[derive(Error, Debug, Copy, Clone)] #[repr(u32)] pub enum NoteError { #[error("You do not own this note")] Forbidden = 0, #[error("Note text is too long")] InvalidLength = 1, #[error("Rating must be between 1 and 5")] InvalidRating = 2, #[error("Note title cannot be empty")] EmptyTitle = 3, #[error("Maximum notes limit reached")] MaxNotesExceeded = 4, } ``` ### 6. Error Testing ```rust #[cfg(test)] mod tests { use super::*; #[test] fn test_invalid_rating() { let result = validate_rating(0); assert_eq!( result.unwrap_err(), NoteError::InvalidRating.into() ); let result = validate_rating(6); assert_eq!( result.unwrap_err(), NoteError::InvalidRating.into() ); } #[test] fn test_valid_rating() { for rating in 1..=5 { assert!(validate_rating(rating).is_ok()); } } } ``` ### 7. Avoid Silent Failures ```rust // ❌ Bad - errors ignored pub fn bad_error_handling(accounts: &[AccountInfo]) -> ProgramResult { let _ = validate_accounts(accounts); // Ignores error! if let Ok(data) = load_data(accounts) { process(data); // What if load_data failed? } Ok(()) // Returns success even if operations failed! } // ✅ Good - errors propagated pub fn good_error_handling(accounts: &[AccountInfo]) -> ProgramResult { validate_accounts(accounts)?; let data = load_data(accounts)?; process(data)?; Ok(()) } ``` --- ## Summary **Key Takeaways:** 1. **Always return `ProgramResult`** from instruction handlers 2. **Use custom errors** for specific failure modes 3. **Implement `From` trait** to convert custom errors to `ProgramError` 4. **Use `?` operator** for clean error propagation 5. **Add context with `msg!`** for better debugging 6. **Fail fast** - return errors immediately 7. **Document error codes** for client developers 8. **Test error cases** as thoroughly as success cases **Error Handling Pattern:** ```rust use solana_program::{ entrypoint::ProgramResult, program_error::ProgramError, msg, }; use thiserror::Error; // 1. Define custom errors #[derive(Error, Debug, Copy, Clone)] pub enum MyError { #[error("Descriptive error message")] SpecificError, } // 2. Implement From conversion impl From for ProgramError { fn from(e: MyError) -> Self { ProgramError::Custom(e as u32) } } // 3. Use in program pub fn my_instruction(accounts: &[AccountInfo]) -> ProgramResult { // Validate if invalid_condition { msg!("Detailed error context"); return Err(MyError::SpecificError.into()); } // Propagate errors with ? let data = load_data(accounts)?; Ok(()) } ``` **Remember:** Good error handling is not optional—it's essential for security, debugging, and user experience.