Error Handling On-Chain
Programs communicate failure through ProgramResult (Result<(), ProgramError>) or custom error enums mapped to codes. Clients decode these codes off-chain for UX and debugging.
Recipe
use solana_program::program_error::ProgramError;
fn require_init(data_len: usize, need: usize) -> Result<(), ProgramError> {
if data_len < need {
return Err(ProgramError::InvalidAccountData);
}
Ok(())
}When to reach for this:
- Replacing panics with recoverable instruction failures.
- Defining domain-specific errors (slippage, expired, unauthorized).
- Propagating errors from helpers with
?.
Working Example
use solana_program::program_error::ProgramError;
#[repr(u32)]
pub enum VaultError {
InsufficientFunds = 6000,
Unauthorized = 6001,
}
impl From<VaultError> for ProgramError {
fn from(e: VaultError) -> Self {
ProgramError::Custom(e as u32)
}
}
pub fn withdraw(amount: u64, balance: u64) -> Result<(), ProgramError> {
if amount > balance {
return Err(VaultError::InsufficientFunds.into());
}
Ok(())
}What this demonstrates:
- Custom errors use
ProgramError::Custom(code). Fromimpl enables?in instruction handlers.- Reserve code ranges per program to avoid collisions with Anchor.
Deep Dive
Error Layers
| Layer | Type |
|---|---|
| Runtime | ProgramError::InvalidAccountData |
| App | ProgramError::Custom(u32) |
| Anchor | error_code attribute macro |
Mapping Off-Chain
- Anchor IDL embeds error names.
- Native programs document codes in README or IDL JSON.
Rust Notes
type VaultResult<T> = Result<T, ProgramError>;Gotchas
- Panic on expect - Aborts without a custom code.. Fix: Return
Errwith explicit variant. - Code collision - 6000 may clash with other programs in same tx logs.. Fix: Namespace codes; document per program.
- Leaking internals - Detailed logs reveal exploit hints.. Fix: Log generic message publicly; detail in private sim.
- Ignoring CPI errors - Must propagate
invokeResult.. Fix: Use?or map to your enum. - Too many variants - Large error enums still cheap - but keep client mapping updated.. Fix: Version IDL alongside program.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
Anchor error! macro | Auto IDL export | Non-Anchor builds |
| Only ProgramError builtins | Minimal codes | Product UX |
| Events for success paths | Rich observability | Not a substitute for errors |
FAQs
What is ProgramResult?
Alias for Result<(), ProgramError>.
Can I return data on error?
No - failed instruction rolls back all changes.
Anchor error offset?
Anchor adds offset 6000+ for custom codes in 0.32.x.
thiserror on-chain?
Works if no std-only features enabled.
Log inside error path?
Allowed but costs CUs - keep short.
Client decode?
Match custom code to enum off-chain.
InvalidAccountData vs Custom?
Builtins are portable; custom need program IDL.
Result in helpers?
Yes - bubble with ? in instruction handler.
assert! macro?
Avoid - use explicit returns.
Multiple errors in one tx?
First failing instruction fails whole tx (unless handled).
Simulation errors?
Same codes as on-chain execution.
Errors in CPI?
Bubble up or map deliberately - do not swallow.
Related
- Error Handling Basics - Section intro
- Custom Error Enums - IDL-friendly codes
- Debugging Failed Transactions - Reading failures
Stack versions: This page was written for Agave 4.1.1, Solana CLI 3.0.10, Anchor 0.32.1, anchor-lang 0.32.1, Rust 1.91.1, @solana/kit 7.0.0, Surfpool 0.12.0, and LiteSVM 0.6.x.