Instruction Handlers
Instruction handlers are public functions inside #[program]. They receive Context<T> where T is the validated accounts struct, plus any Borsh-deserialized arguments.
Recipe
pub fn deposit(ctx: Context<Deposit>, amount: u64) -> Result<()> {
require!(amount > 0, VaultError::InvalidAmount);
// business logic
Ok(())
}When to reach for this: You implement business logic for an on-chain instruction.
Working Example
#[program]
pub mod vault {
use super::*;
pub fn deposit(ctx: Context<Deposit>, amount: u64) -> Result<()> {
let vault = &mut ctx.accounts.vault;
vault.total = vault.total.checked_add(amount).ok_or(VaultError::Overflow)?;
emit!(Deposited { amount, vault: vault.key() });
Ok(())
}
}
#[derive(Accounts)]
pub struct Deposit<'info> {
#[account(mut, seeds = [b"vault"], bump = vault.bump)]
pub vault: Account<'info, VaultState>,
#[account(mut)]
pub depositor: Signer<'info>,
}What this demonstrates:
Context<Deposit>wraps accounts and program_id- Instruction args deserialize after account validation
- Return
Result<()>propagates custom errors ctx.bumpsexposes canonical bumps from seeds
Deep Dive
Context Fields
| Field | Use |
|---|---|
ctx.accounts | Mutable/immutable validated accounts |
ctx.program_id | Current program pubkey |
ctx.bumps | Map of bump seeds from constraints |
ctx.remaining_accounts | Extra accounts not in struct |
Keep handlers small; delegate to module functions for complex logic.
Gotchas
- Heavy work in handler - CU limits exceeded.. Fix: Extract helpers; avoid redundant deserializes.
- Ignoring ctx.bumps - Re-search bumps in CPI.. Fix: Use
ctx.bumps.vaultfrom seeds name. - Mutating without persist intent - Forgot account is Account<T>.. Fix: Changes serialize on drop if mut.
- Wrong Result type - Handlers must return Result for fallible paths.. Fix: Use
Result<()>or return data APIs. - Using remaining_accounts blindly - Account substitution attacks.. Fix: Validate owner and discriminator.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
| Module helpers | Shared logic across handlers | Inline for tiny programs |
| return_data APIs | Return bytes to caller | Most instructions use accounts for output |
| Native instruction match arms | No Anchor | Anchor Context ergonomics |
FAQs
What Anchor version is assumed?
0.32.1 throughout this section.
Can a PDA be a Signer in accounts struct?
No. Use seeds constraints and CPI signing.
Where is bump stored?
In your account struct field, set at initialization.
How do clients derive PDAs?
Use @solana/kit 7.0.0 with matching seed bytes.
What program id is used for PDAs?
Your Anchor program's declare_id address.
Do seeds include bump?
Not in seeds array; bump is separate parameter to find_program_address.
How to debug PDA failures?
Compare logged keys; verify seeds and program id client-side.
Are PDAs rent-exempt?
Yes when holding data; fund with payer on init.
Can one PDA sign multiple CPIs in one ix?
Yes with same signer seeds for each CPI.
What to read next?
See Related links for deeper handlers topics.
Related
- Instruction Arguments - typed inputs
- Accessing Accounts - ctx.accounts details
- The #[program] Module - module macro
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.