Anchor Errors & Events
Errors, events, and logs are how an Anchor program communicates outcomes that account state alone does not capture: fail with a stable code, publish a typed success record for indexers, or leave a short human trail for engineers. Anchor 0.32.1 wires those channels into the IDL so clients and indexers share one contract with the on-chain binary.
#[error_code] & require! is the hands-on entry; Custom Error Messages, Events with emit!, emit_cpi! & Self-CPI Events, Logging & CU Cost, and Client-Side Error Decoding zoom into each surface. This page is the map underneath.
Summary
- Anchor splits observability into typed errors (
#[error_code],require!,error!), structured events (#[event],emit!,emit_cpi!), and budgeted logs (msg!), all exportable through the IDL for off-chain consumers. - Insight: Users never see your Rust
?chain. They see simulation failures, explorer log lines, and whatever your dApp maps fromCustom(u32). Design those surfaces on purpose or you ship opaque "Transaction failed" UX and CU waste. - Key Concepts:
#[error_code],require!/error!,#[msg], constraint@ Error,#[event]/emit!/emit_cpi!, log CU cost, IDL error tables, simulation-first decoding. - When to Use: Designing failure modes for vaults, markets, or admin paths; instrumenting success for analytics; wiring @solana/kit 7.0.0 or Anchor clients to decode failures; explaining program observability to frontend and indexer teams.
- Limitations/Trade-offs: Every log and event spends compute units; every error code is a public API you must version; failed instructions roll back state; logs and events are public chain data (no secrets).
- Related Topics: error codes and require, custom messages, emit and emit_cpi, logging CU, client-side decoding.
Foundations
An Anchor instruction handler returns Result<()>. Success commits account mutations; failure aborts the instruction (and transaction) and reverts those mutations. There is no partial commit of "error fields." The failure itself is the signal.
That design pushes observability into execution metadata and the IDL, not into durable last_error accounts (unless you write such state on a success path). Three channels fill the gap:
| Channel | Primary consumer | Failure semantics | Anchor surface |
|---|---|---|---|
| Error | Runtime, wallets, dApp decoders | Instruction fails; state rolls back | #[error_code], require!, error!, constraint @ |
Log (msg!) | Engineers, explorers, support | Visible for executed steps up to failure | msg!, feature-gated debug |
| Event | Indexers, analytics, webhooks | Emitted on paths that run far enough to log | #[event], emit!, emit_cpi! |
Anchor handler (Result<()>)
|
+-- require! / error! / ? --> Err(code) --> fail tx + logs so far
|
+-- msg!("...") --> CU cost --> logMessages
|
+-- emit! / emit_cpi! --> #[event] --> indexers / IDL consumers
|
+-- Ok(()) --> commit account writesErrors are the control-flow contract. Prefer typed variants for user-facing rules and keep messages product-safe. Logs are a short, prefixed, feature-gated debug channel. Events are the structured channel for off-chain systems that should not scrape prose.
All three share one property: transaction logs are public. Seeds, private keys, PII, and exploit-ready internals do not belong there.
Anchor's advantage over raw ProgramError::Custom is IDL integration: anchor build exports error names, codes, optional #[msg] strings, and event schemas into target/idl/*.json. Clients pin that artifact to the deployed program.
Mechanics & Interactions
#[error_code], require!, and error!
Define domain failures as an enum. Anchor assigns stable numeric codes (conventionally starting at 6000) and includes them in the IDL:
#[error_code]
pub enum VaultError {
#[msg("Vault is paused")]
Paused,
#[msg("Insufficient balance")]
InsufficientFunds,
#[msg("Math overflow")]
Overflow,
}
pub fn withdraw(ctx: Context<Withdraw>, amount: u64) -> Result<()> {
require!(!ctx.accounts.vault.paused, VaultError::Paused);
require!(
ctx.accounts.vault.balance >= amount,
VaultError::InsufficientFunds
);
ctx.accounts.vault.balance = ctx.accounts.vault.balance
.checked_sub(amount)
.ok_or(VaultError::Overflow)?;
Ok(())
}| Macro / form | Role |
|---|---|
require!(cond, Error::Variant) | Boolean assert; fails with the variant |
require_eq! / require_keys_eq! | Equality / pubkey checks |
error!(Error::Variant) / err! | Early return without a boolean guard |
? on Result | Propagate or map CPI and helper errors |
Use constraints for account-time rules so invalid accounts never reach handler body logic:
#[account(constraint = !pool.disabled @ SwapError::PoolDisabled)]
pub pool: Account<'info, Pool>,Append-only variants so codes never silently remap. Reordering renumbers codes and breaks every client map. Treat the enum like a versioned public API.
Deep dive: #[error_code] & require!.
Custom messages with #[msg]
#[msg("...")] attaches a default English string to each variant. That string lands in the IDL for explorers, AnchorError, and generated tables. It is not a localization system and not a place for dynamic values. Keep strings short, user-safe, and stable.
Off-chain, map codes to product copy, i18n keys, and retry hints. On-chain English is a tooling fallback, not the only UX surface. Details: Custom Error Messages.
Events with emit!
When dashboards or Geyser plugins need reliable fields, free-text msg! parsing becomes a liability. Mark a Borsh-friendly struct with #[event] and publish it on success paths:
#[event]
pub struct PositionOpened {
pub owner: Pubkey,
pub market: Pubkey,
pub size: u64,
pub timestamp: i64,
}
emit!(PositionOpened {
owner: ctx.accounts.owner.key(),
market: ctx.accounts.market.key(),
size,
timestamp: Clock::get()?.unix_timestamp,
});Events appear in the IDL. Fields must be public and serializable. Prefer keys and amounts over large blobs. Account state remains authoritative. Failed instructions do not commit state, so design failure UX around error codes, not durable "error events." Deep dive: Events with emit!.
emit_cpi! and self-CPI delivery
Classic emit! logs event data in a form many tools understand, often via an internal self-CPI pattern. emit_cpi! makes the CPI path explicit so indexers that depend on CPI observation capture events more reliably:
emit_cpi!(LiquidityAdded {
provider: ctx.accounts.provider.key(),
amount,
});| Macro | CU cost | Typical fit |
|---|---|---|
emit! | Lower | Tests, simple pipelines, lower-stakes telemetry |
emit_cpi! | Higher | Production indexers with strict capture requirements |
Standardize one style per program so parsers stay simple. Budget CU after switching to emit_cpi! on hot paths. Details: emit_cpi! & Self-CPI Events.
Logging and compute units
msg! invokes the log syscall. Cost scales with call count and payload size. On a tight CU budget, a loop of formatted logs can tip a transaction into compute exceeded.
Practical rules:
- Prefer one short line per decision over multi-line dumps.
- Gate verbose logging behind Cargo features on mainnet builds.
- Use stable prefixes (
VAULT:deposit_ok) for ops grep. - Prefer events for production observability; reserve
msg!for dev, Surfpool, LiteSVM, and short incidents. - Never log secrets (seeds, private metadata, public billboard data only).
Profile with sol_log_compute_units and set a client compute budget when instructions approach limits. See Logging & CU Cost.
Client-side decoding
Wallets often show a generic failure. Your dApp should do better:
- Run
simulateTransaction(or confirm and fetch meta) with the same accounts and data. - Extract the failing program ID and custom code (decimal or hex, e.g.
0x1770= 6000). - Map through IDL
errorsvia@coral-xyz/anchor'sAnchorError, Codama-generated tables, or a pinned lookup for @solana/kit 7.0.0. - Show a product message, optional i18n string, and a code-specific retry hint.
Simulate message --> InstructionError + logs
|
v
program_id + code --> IDL / generated table
|
v
UI string + retry hintPin the decoder artifact to the deployed program hash. Stale IDLs cause wrong error names in support tickets. Always decode with (program_id, code) in multi-program transactions. Patterns: Client-Side Error Decoding.
Observability map for Anchor teams
| Question | Channel | On-chain tool | Off-chain tool |
|---|---|---|---|
| Should this ix fail? | Error | require! / error! / @ Variant | Decode code; block send on sim fail |
| What step ran? | Log | Feature-gated msg! | Explorer / sim logMessages |
| What should indexers store? | Event | emit! / emit_cpi! | Geyser plugin, custom indexer |
| Is state correct after success? | Account data | Writes under owner rules | RPC getAccount / subscriptions |
Errors decide control flow. Logs explain engineers. Events feed machines. Account state settles disputes.
Advanced Considerations & Applications
Constraint vs handler errors. Put identity and account-shape rules in #[derive(Accounts)] with @ YourError::Variant. Put business rules that need computed amounts or CPI results in require! inside the handler.
CPI boundaries. Callee errors may surface in outer logs. Rethrow, wrap into your own UX code, or leave the callee code honest. Decode with (program_id, code), not code alone.
Security and UX. Granular errors help users; over-granular ones can teach attackers which check failed. Prefer stable categories. Put exploit-hunting detail behind gated non-mainnet logs, not permanent #[msg] strings.
Compute budget. Log or event spam can cause failures only under load. Profile CU after upgrades (this site pins Agave 4.1.1) and treat log removal as a real performance change.
Versioning and tests. Tie program binary, IDL, and error table together. Archive decoder artifacts per deploy. Assert specific codes in Anchor/LiteSVM tests so silent renumbering fails CI.
| Approach | Strength | Weakness | Best fit |
|---|---|---|---|
| Built-in constraint errors only | Fast scaffolding | Weak product UX | Prototypes |
#[error_code] + IDL | Stable UX, i18n-ready | Must version carefully | User-facing protocols |
Heavy msg! everywhere | Fast local debug | CU, noise, public data | Dev builds, short incidents |
emit! / emit_cpi! | Indexer-friendly schema | CU + schema maintenance | Analytics, protocol telemetry |
| Sim + decode in dApp | Best user messaging | Needs maintained IDL pin | Wallets, trading UIs, support |
Common Misconceptions
- "I'll return a free-form string and the wallet will show it." Clients primarily see codes and log lines. Design stable
#[error_code]variants and decode them yourself. - "
msg!is free debugging." Each call spends compute units and can fail tight budgets. - "Events are durable storage." They are structured log payloads; account state is the source of truth, and failed instructions do not commit state.
- "I can log seeds; only my team reads logs." Transaction logs are public chain data.
- "Custom error 6001 always means the same thing on Solana." Codes are per program; always pair with program ID.
- "Reordering enum variants is a pure refactor." It renumbers codes and breaks client maps; append only.
- "
emit!andemit_cpi!are interchangeable for every indexer." Capture reliability and CU cost differ; pick one style and verify your pipeline. - "On-chain
#[msg]is enough for internationalization." Map codes to locale strings off-chain.
FAQs
What is the difference between an error, a log, and an event in Anchor?
An error fails the instruction with a code from #[error_code] and rolls back state. A log is a free-text msg! line for humans. An event is a typed #[event] payload so indexers can decode fields via the IDL.
How does Anchor assign error codes?
#[error_code] enums are exported in the IDL with numeric codes conventionally starting at 6000, plus optional #[msg] strings for tooling and clients.
When should I use require! vs constraint @ Error?
Use account constraints for validation available at context build time. Use require! or error! for handler-time business rules, computed values, and post-CPI checks.
What is the difference between error! and require!?
require!(condition, Error::V) asserts a boolean and returns the error when false. error!(Error::V) (or err!) returns that error immediately without a condition. Both become the same class of IDL-mapped failure.
Why does msg! cost compute units?
Logging is a runtime syscall. More calls and larger strings consume more of the transaction compute budget.
Should production programs keep verbose msg! calls?
Usually no. Prefer feature-gated verbose logs, short permanent breadcrumbs only where ops value is high, and structured events for indexer needs.
emit! vs emit_cpi! which should I pick?
emit! is cheaper and fine for many apps and tests. emit_cpi! costs more CU but improves capture reliability for CPI-dependent indexers. Standardize per program and validate on devnet.
Are events visible if the transaction fails?
Failed instructions do not commit program state. Design failure UX around error codes and logs up to the failure point, not durable error-event accounts.
How do @solana/kit clients decode Anchor errors?
Match the custom code from simulation or confirmed meta against IDL or Codama-generated tables, pinned to the program version your app talks to. Always include program ID in multi-instruction transactions.
Can account constraints return custom errors?
Yes. Use constraint = ... @ MyError::Variant so validation failures map to your IDL table when you need product-specific UX.
How should multi-program transactions be decoded?
Walk the log invoke stack, identify which program ID failed, then map that program's code table. Never assume a bare integer is yours.
Do events replace account subscriptions for indexers?
They complement them. Events give explicit domain facts; account subscriptions give state diffs. Many production pipelines use both.
How do I keep error tables in sync with deploys?
Publish the IDL in CI alongside the program artifact, version it with the deploy hash, and refuse client releases that pin the wrong table.
Where does i18n for errors belong?
Off-chain. Map stable codes to locale strings in the client; do not rely on on-chain English #[msg] as the only user text.
How does this map to native Solana without Anchor?
The runtime channels are the same: ProgramError, log syscalls, and optional binary log data. Without Anchor you own more of the table publishing story and lose the convenience macros.
Related
- #[error_code] & require! - Declaring and asserting typed errors
- Custom Error Messages -
#[msg]strings and stable UX copy - Events with emit! - Structured
#[event]payloads for indexers - emit_cpi! & Self-CPI Events - Reliable capture via explicit CPI
- Logging & CU Cost - Budget
msg!and production observability - Client-Side Error Decoding - Map IDL codes in TypeScript clients
Stack versions: This page was written for Agave 4.1.1, Solana CLI 3.0.10, Anchor 0.32.1, Rust 1.91.1, and @solana/kit 7.0.0.