Anchor Development Ergonomics
Learn how to keep Anchor programs readable and trustworthy with stronger constraints, clearer errors, thinner handlers, better events, and a more disciplined testing strategy.
By this point in the section, you have already built enough Anchor code to see the real problem.
Getting a program to work once is not the hard part.
Keeping it readable, reviewable, and safe as it grows is the hard part.
That is what this lesson is about.
The Real Goal
Anchor ergonomics is not about making code look fancy.
It is about reducing the number of places bugs can hide.
Good ergonomics means:
- validation rules are easy to see
- authority rules are easy to explain
- handlers are short enough to reason about
- failures are specific
- tests describe the real risk surface
That is the standard you want.
1. Push Validation Into Account Constraints First
The official Anchor account constraint system exists for a reason.
If a rule can be expressed in the account context, do not bury it deep in the handler unless you have a strong reason.
#[account(
mut,
has_one = authority,
constraint = vault.mint == mint.key() @ ErrorCode::InvalidMint,
)]
pub vault: Account<'info, VaultState>,Why this is better than manual checks hidden later:
- the rule is visible near the account itself
- the handler stays shorter
- reviewers can see the security model faster
- error locality improves
This is not about style.
It is about making the program easier to trust.
2. Keep Handlers Thin
A good handler should usually do one short sequence:
- rely on the account context for setup and validation
- perform the minimal business logic
- write the state change
- emit an event if the action matters externally
When handlers become long, review gets harder.
That is when people start missing authority bugs and stale state bugs.
A thin handler looks like this:
pub fn complete_escrow(ctx: Context<CompleteEscrow>) -> Result<()> {
let escrow = &mut ctx.accounts.escrow;
require!(escrow.status == EscrowStatus::Funded, ErrorCode::InvalidState);
transfer_from_vault(ctx.accounts)?;
escrow.status = EscrowStatus::Completed;
Ok(())
}The helper functions can still be complex if needed.
But the instruction entrypoint should stay easy to read.
3. Use Specific Error Codes
Anchor's custom error system is there to make failures explicit.
Use it properly.
#[error_code]
pub enum ErrorCode {
#[msg("Caller is not authorized for this action")]
Unauthorized,
#[msg("Vault mint does not match expected mint")]
InvalidMint,
#[msg("Escrow is not in the required state")]
InvalidState,
}Good errors do three things:
- tell you what failed
- tell the client what kind of failure happened
- make tests easier to write and review
Bad errors hide intent.
Specific errors expose intent.
4. Emit Events For State Changes That Matter
Anchor supports events directly.
Use them for meaningful transitions, not for noise.
#[event]
pub struct EscrowCompleted {
pub escrow: Pubkey,
pub maker: Pubkey,
pub taker: Pubkey,
pub amount: u64,
}Events are useful when:
- off-chain systems need to index state changes
- you want a clean activity trail for important actions
- business-critical transitions should be visible without re-parsing every account later
Do not emit events just because the framework supports them.
Emit them when the action actually matters to observers.
5. Keep Authority Rules Explainable In One Sentence
This is one of the best review tests you can apply.
For every mutating instruction, ask:
who is allowed to do this, and why?
If the answer takes a paragraph, your authority model is probably too muddy.
A good answer sounds like this:
- "Only the stored admin can mint because the config account has
has_one = admin." - "Only the recorded taker can complete the escrow when the status is
Funded." - "Only the PDA derived from the escrow key can sign the vault transfer."
If you cannot explain the rule cleanly, reviewers will struggle too.
6. Treat PDA Seed Formulas Like Schemas
The PDA lesson already covered this at the mechanics level.
At the ergonomics level, the important rule is stronger:
seed formulas should be stable, documented, and reused consistently.
A seed formula is not a random helper expression.
It is part of the contract of your program.
That means:
- choose the namespace deliberately
- keep seed order fixed
- keep the same derivation rule across creation and later validation
- test the wrong-seed path explicitly
This is one of the easiest places for a program to become confusing over time.
7. Separate Business Logic From Transport Logic
Anchor instructions do two different jobs at once:
- they receive accounts and instruction inputs
- they express business rules and state changes
Do not let those concerns get tangled more than necessary.
Bad shape:
- parsing inputs
- validating authority
- checking state
- performing CPI
- mutating three accounts
- formatting side effects
all in one long handler
Better shape:
- context expresses most validation
- handler expresses the business step
- helper functions handle repeated logic like vault transfers or state updates
That makes review much easier.
8. Testing Should Follow The Risk, Not The Tutorial
The official Anchor docs still make anchor test the default local integration path.
That is still the right baseline.
Start there.
Then add more focused tools only when the program complexity justifies them.
A sensible progression now looks like this:
anchor testfor full local integration flow- LiteSVM when you want faster in-process tests in Rust, TS/JS, or Python
- Mollusk when you want very explicit, deterministic account-by-account instruction testing in Rust
The current docs describe LiteSVM as a fast in-process VM and Mollusk as a lightweight harness where accounts are defined explicitly.
That should tell you when to reach for them.
Do not replace clear integration tests with clever simulation tests too early.
The point is to increase confidence, not to collect testing tools for their own sake.
9. Test Failure Paths As First-Class Behavior
A serious Anchor program should not only prove that success works.
It should prove that abuse fails correctly.
For each important instruction, you should usually have tests for at least some of these:
- wrong signer
- wrong PDA seeds
- wrong mint
- wrong state transition
- repeated execution attempt
- overflow or cap violation
That is the real shape of trust.
A happy-path-only suite proves much less than people think.
10. Treat Upgrades And State Evolution Deliberately
As soon as a program has real users or real assets, account layout changes stop being casual refactors.
That means you should start thinking clearly about:
- how account schemas evolve
- whether migrations are needed
- who controls upgrade authority
- how that authority is documented operationally
This lesson is not about full upgrade mechanics.
It is about discipline.
If state evolution is not planned, maintainability is already weak.
A Better Review Checklist
Before shipping or merging a significant Anchor change, ask:
- can every mutating instruction explain its authority rule in one sentence?
- are the PDA derivation rules stable and tested?
- are important token assumptions explicit?
- do custom errors tell you what failed clearly?
- are critical state transitions represented on-chain, not just in the client?
- do tests cover at least the main abuse path for each important instruction?
If several answers are weak, the program is not ready just because the happy path works.
Common Ergonomics Failures
Failure 1: the handler is the only place the rules are visible
That makes review harder than it should be.
Failure 2: every failure becomes a generic error
That slows debugging and hides intent.
Failure 3: PDA rules are copied manually in too many places
That increases the chance of drift.
Failure 4: tests only prove the demo path
That is not enough for stateful or token-moving programs.
What Comes Next
At this point, the main teaching spine of the Anchor section is in place.
The remaining capstone should only make sense if all of these habits are already normal:
- clear authority rules
- clear state transitions
- clear PDA derivation
- clear CPI structure
- clear failure testing
Quick Check
Why is it usually better to express a rule in the account context instead of hiding it deep in the handler?
What should drive your Anchor test plan as the program becomes more serious?