Errors, Logs & Events
Errors, logs, and events are how a Solana program talks back when state alone is not enough: fail with a code, leave a human trail, or publish a structured record for indexers. Assign each channel a job and support gets shorter, CU stays predictable, and dApps stop showing "Transaction failed."
Error Handling Basics is the hands-on entry; Custom Error Enums, Logging with msg!, Emitting Events, Decoding Errors Off-Chain, and Debugging Failed Transactions zoom into each mechanism. This page is the map underneath: how the pieces compose from handler to wallet toast.
Summary
- Programs communicate outcomes through three channels with different contracts: errors fail the instruction (and transaction) with a machine-readable code, logs write human-readable lines into transaction meta, and events serialize typed payloads into logs for reliable off-chain consumers.
- Insight: Users and bots never see your Rust stack traces. They see simulation results, explorer
logMessages, and whatever your client maps fromProgramError::Custom. Design those surfaces on purpose or you ship opaque failures and CU waste. - Key Concepts: ProgramResult / ProgramError, custom error enums (stable u32), msg! compute cost, sol_log_data / Anchor emit!, IDL error tables, simulation-first debugging, public logs (no secrets).
- When to Use: Designing failure modes for a vault or market, instrumenting a hot path, wiring @solana/kit or Anchor clients to decode failures, triaging mainnet/devnet incidents, or explaining program observability to a frontend team.
- Limitations/Trade-offs: Every log and event spends compute units; every custom code is a public API you must version; failed instructions roll back state (and event emission on that path); logs are not an authorization mechanism.
- Related Topics: error handling basics, custom error enums, logging with msg!, emitting events, decoding errors off-chain, debugging failed transactions.
Foundations
A Solana instruction handler is essentially a function that returns ProgramResult (Result<(), ProgramError>). Success commits account mutations for that transaction; failure aborts everything and reverts those mutations. There is no partial commit of "error fields" the way some app servers write an error row and still succeed. The failure itself is the signal.
That design pushes observability into metadata around execution, not into durable "last_error" accounts (unless you build such state on success paths). Three complementary channels fill the gap:
| Channel | Primary consumer | Failure semantics | Typical payload |
|---|---|---|---|
| Error | Runtime, wallets, dApp decoders | Instruction fails; state rolls back | Built-in ProgramError or Custom(u32) |
Log (msg!) | Engineers, explorers, support | Visible for executed steps up to failure | Free-text line(s) |
| Event | Indexers, analytics, webhooks | Emitted on paths that succeed far enough to log; failed ix does not leave durable event state | Borsh/IDL-shaped struct in logs |
Instruction handler
|
+-- check invariants ---- Err(Custom code) ----> fail tx + logs so far
|
+-- msg!("step=...") ---- CU cost -------------> logMessages
|
+-- emit!(Event {..}) --- structured bytes ----> indexers / IDL consumers
|
+-- Ok(()) -------------- commit account writesErrors are the control-flow contract. Prefer typed custom codes for user-facing rules (InsufficientFunds, SlippageExceeded) and built-ins for generic argument or account shape failures when they truly apply. Logs are a debug and ops channel: short, prefixed, feature-gated when possible. Events are the structured channel for off-chain systems that should not scrape prose.
All three share one property: transaction logs are public. Treat messages, log lines, and event fields as billboard data. Seeds, private keys, PII, and exploit-ready internals do not belong there.
Mechanics & Interactions
Custom error enums: stable codes, not free-form strings
On-chain Rust does not return String errors to the client in a useful way. Clients see a program ID plus an error code (often hex in explorers: custom program error: 0x1770). Your job is to make that number stable, documented, and decodable.
Native programs typically define a #[repr(u32)] enum and implement From into ProgramError::Custom. Anchor 0.32.1 uses #[error_code] enums that land in the IDL with names and optional #[msg("...")] strings, conventionally offset from 6000. Either way:
- Append-only variants so existing codes never silently remap.
- Publish the table with the program version (IDL JSON or a hand-maintained
errors.jsonfor native). - Namespace in the UI by program ID when a transaction has multiple programs, because the same
u32can mean different things in different programs.
Details and patterns live in Custom Error Enums and Error Handling Basics. The explainer point: the enum is a public API surface, as important as instruction discriminators.
msg! costs: budget logs like hot-path code
solana_program::msg! (and Anchor wrappers) invoke the log syscall. Cost scales with calls and payload size. On a tight CU budget, a loop of formatted logs can tip a transaction into compute exceeded and look mysterious until you read the simulation.
Practical rules:
- Prefer one short line per decision over multi-line dumps of account bytes.
- Gate verbose logging behind Cargo features or cfg so mainnet builds stay quiet.
- Use stable prefixes (
VAULT:deposit_ok) so ops can grep without archaeology. - Never log secrets (seeds, private metadata, or payloads you would not store publicly).
Logging shines for local validators, Surfpool traces, LiteSVM tests, and short incident instrumentation. It is a poor long-term substitute for typed errors (UX) or events (indexers). See Logging with msg!.
Structured events: machine-readable success (and progress)
When dashboards, risk engines, or Geyser plugins need reliable fields, free-text msg! parsing becomes a liability. Anchor emit! / emit_cpi! and native sol_log_data conventions put typed structs into the log stream with an IDL- or docs-defined schema.
Events still cost CU and remain public. Keep payloads small (pubkeys, amounts, enums), version layouts when fields break, and treat account state as authoritative. Events describe outcomes for consumers; they are not a second ledger. Failed instructions do not commit program state, so do not rely on "error events" as durable truth. Decode error codes for failure UX; use events for successful domain outcomes. Deep dive: Emitting Events.
Decoding off-chain: from Custom(n) to product language
Wallets often show a generic failure. Your dApp (or support tool) 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).
- Map through IDL
errors(Anchor) or a published table (native) via helpers generated for @solana/kit 7.0.0 or your own lookup. - Show a product message, optional i18n string, and a retry hint that depends on the code (slippage vs insufficient funds vs unauthorized).
Pin the decoder artifact to the deployed program hash. Stale IDLs are a common cause of "wrong error name" support noise. Full client patterns: Decoding Errors Off-Chain.
Debugging failed transactions: logs up to the cut
When a transaction executes and fails, meta typically includes logMessages for work performed until the error. Use that stack to see which program was invoked, which CPI was active, and which custom code ended the run. When a transaction never lands (blockhash, drop, insufficient fee), you get a different problem class: no program logs because the program never ran.
A reliable loop:
User report / CI fail
|
v
Simulate same message --> read logs + err
|
v
Map code via IDL --> form hypothesis
|
v
Reproduce in LiteSVM / Surfpool --> add regression test
|
v
Fix (code, accounts, CU, or client) + strip temporary logsCLI (solana confirm -v, sim APIs), explorers, Surfpool 0.12.0 traces, and LiteSVM unit tests all consume the same fundamental signals: codes and logs. Workflow details: Debugging Failed Transactions.
Observability map for programs
Use one shared map across program and client teams:
| Question | Channel | On-chain tool | Off-chain tool |
|---|---|---|---|
| Should this ix fail? | Error | Err(MyError::..) / require! | Decode code; block send on sim fail |
| What step ran? | Log | msg! / feature-gated vlog | Explorer / sim logMessages |
| What business fact should indexers store? | Event | emit! / sol_log_data | 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. Mixing those jobs (for example parsing prose logs for critical accounting) creates fragile systems.
Advanced Considerations & Applications
CPI boundaries complicate the picture. When your program invokes another, the callee's error may surface in the outer logs. Map carefully: rethrow, wrap into your own UX code, or leave the callee code so multi-program explorers stay honest. Always decode with (program_id, code), not code alone.
Security and UX pull opposite ways. Granular errors help users fix inputs; over-granular ones can teach attackers which check failed. Prefer stable categories over "debug dump" production messages. Put exploit-hunting detail behind gated non-mainnet logs, not permanent #[msg] strings.
Compute budget matters on markets and multi-hop routes. Failures that appear only under load because of log spam are a real incident class. Profile CU in simulation after upgrades (this site pins Agave 4.1.1) and treat log removal as a legitimate performance change.
Versioning ties program binary, IDL, and error table together. Rarely upgraded programs freeze the error API; plan codes early. Upgradeable programs should archive decoder artifacts per deploy so historical txs stay explainable.
Testing should assert codes, not only success. LiteSVM and Anchor tests that expect Custom(6001) catch silent renumbering. Client tests should decode fixture sim errors the same way production does.
| Approach | Strength | Weakness | Best fit |
|---|---|---|---|
Built-in ProgramError only | Simple, portable | Weak product UX | Internal tools, early prototypes |
| Custom enums + 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 |
| Structured events | Indexer-friendly schema | CU + schema maintenance | Analytics, protocol telemetry |
| Sim + decode in dApp | Best user messaging | Needs maintained tables | Wallets, trading UIs, support tools |
Common Misconceptions
- "I'll return a string error and the wallet will show it." Clients primarily see codes and log lines; design stable
u32codes and decode them yourself. - "Logs are free debugging." Each
msg!spends compute units and can cause failures on tight budgets. - "Events are durable storage." They are structured log payloads for consumers; account state is the source of truth, and failed instructions do not commit state.
- "I can log seeds and admin keys; only my team reads logs." Transaction logs are public chain data visible in explorers and archives.
- "Custom error 6001 always means the same thing on Solana." Codes are per program; always pair with program ID.
- "If the explorer shows no logs, my program panicked without a code." Many "failures" never execute (expiry, drop, fee). Separate landing problems from program errors.
- "Reordering enum variants is a pure refactor." It renumbers codes and breaks every client map; append only.
FAQs
What is the difference between an error, a log, and an event?
An error fails the instruction with a code and rolls back state. A log is a free-text line in transaction meta for humans. An event is a typed payload written into logs so indexers can decode fields reliably.
Why do instruction handlers return ProgramResult?
So the runtime can commit on Ok(()) or abort the entire transaction on Err(ProgramError), keeping multi-instruction atomicity.
When should I use ProgramError::Custom instead of built-ins?
When the failure is domain-specific and clients need a stable, named code for UX or automation. Use built-ins for generic invalid-argument or account issues that truly match those semantics.
How does Anchor assign error codes?
#[error_code] enums are exported in the IDL with numeric codes conventionally starting at 6000, plus optional message strings for tooling.
Why does msg! cost compute units?
Logging is a runtime syscall; more calls and larger strings consume more of the transaction compute budget, which can cause the instruction to fail if the budget is tight.
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.
Are events visible if the transaction fails?
Failed instructions do not commit program state. Design failure UX around error codes and the logs captured up to the failure point, not around durable "error event" accounts unless you build that on a success path deliberately.
How do @solana/kit clients decode errors?
They match the custom code from simulation or confirmed meta against IDL or generated error tables, ideally pinned to the program version your app talks to.
What is the first step when a user says a transaction failed?
Determine whether it landed and executed. If yes, pull logs and the error code (or simulate the same message). If it never executed, debug fees, blockhash, and landing path first.
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.
Can panics replace typed errors?
No for user-controlled paths. Panics abort without a clean product error API and are harder to map in clients. Return Err with a deliberate code.
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 IDL or errors.json in CI alongside the program artifact, version them 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] strings as the only user text.
How does this map change for Pinocchio or native-only stacks?
The runtime channels are the same: ProgramError, log syscalls, and optional binary log data. You own more of the IDL/table publishing story without Anchor's macros.
Related
- Error Handling Basics - ProgramResult and returning failures
- Custom Error Enums - Stable, IDL-friendly error codes
- Logging with msg! - CU-aware human logs
- Emitting Events - Structured events for indexers
- Decoding Errors Off-Chain - Map codes in clients
- Debugging Failed Transactions - Simulation and log triage
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.