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.
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 from Custom(u32). Design those surfaces on purpose or you ship opaque "Transaction failed" UX and CU waste.
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.
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:
Errors 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.
Append-only variants so codes never silently remap. Reordering renumbers codes and breaks every client map. Treat the enum like a versioned public API.
#[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.
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:
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!.
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:
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.
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 errors via @coral-xyz/anchor's AnchorError, 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 hint
Pin 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.
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.
"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! and emit_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.
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.