Files
gh-tenequm-claude-plugins-s…/skills/solana-development/references/error-handling.md
2025-11-30 09:01:25 +08:00

801 lines
19 KiB
Markdown

# 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<CustomError> for ProgramError`:
```rust
impl From<NoteError> 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<MyError> 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.