A wallet popup that says Transaction simulation failed has compressed several
useful signals into one bad message. The RPC response can identify the failed
instruction, the active program, its CPI path, custom error, logs, and compute
use. You need to preserve and read those signals in the right order.
This guide gives you the Five-Layer Debug Trace, a repeatable workflow for
diagnosing Solana execution failures before and after submission. It covers raw
RPC simulation, Anchor errors, account-state drift, and the boundary between a
program failure and an RPC delivery problem.
If the transaction structure itself is unfamiliar, read
Solana transactions explained first.
Simulation runs the transaction without broadcasting it
The simulateTransaction RPC method executes a serialized transaction against
the chain data visible at the requested commitment. It does not broadcast the
transaction or create an on-chain signature status.
The response can include:
- err, the transaction-level error
- logs, including program invocation, messages, compute use, and failure lines
- innerInstructions, the CPIs made by each top-level instruction
- unitsConsumed, useful for compute-budget diagnosis
- returnData, when a program sets return data
- requested account data and balance changes
That makes simulation a debugger, not a promise. An account can change after
simulation. A blockhash can expire. Another transaction can consume liquidity
or modify a counter first. A successful simulation means the transaction worked
against the simulated bank state with that message and configuration.
Use a simulation helper that preserves the full response
Do not reduce simulation to if (err) throw. Store the context slot and all
diagnostic fields. The following helper uses raw JSON-RPC so the response shape
stays visible:
type SimulationValue = {
err: unknown | null;
logs: string[] | null;
innerInstructions?: unknown[] | null;
unitsConsumed?: number;
returnData?: unknown | null;
replacementBlockhash?: {
blockhash: string;
lastValidBlockHeight: number;
} | null;
};
export async function simulateBase64Transaction(
rpcUrl: string,
transactionBase64: string,
) {
const response = await fetch(rpcUrl, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
jsonrpc: '2.0',
id: 1,
method: 'simulateTransaction',
params: [
transactionBase64,
{
encoding: 'base64',
commitment: 'confirmed',
replaceRecentBlockhash: true,
innerInstructions: true,
},
],
}),
});
if (!response.ok) throw new Error(`RPC HTTP ${response.status}`);
const payload = (await response.json()) as {
error?: { message: string };
result?: { context: { slot: number }; value: SimulationValue };
};
if (payload.error || !payload.result) {
throw new Error(payload.error?.message ?? 'Missing simulation result');
}
return payload.result;
}replaceRecentBlockhash: true is helpful for diagnosis because the RPC supplies
a valid blockhash for the simulation. If you need to verify real signatures,
use the original blockhash and sigVerify: true instead. Keep the commitment
consistent with the state and blockhash assumptions used elsewhere in your
transaction flow.
The Five-Layer Debug Trace
Read failures from the outside inward. Each layer narrows the search space
before you touch program code.
1. Classify the transaction error
Start with value.err. A simple value such as BlockhashNotFound points to
transaction lifetime or RPC context. An instruction failure has a shape like:
{
"InstructionError": [2, { "Custom": 6001 }]
}The first number is the zero-based index of the failed top-level instruction.
Here, instruction 2 failed. Map it back to the message you built, including
any compute-budget or account-creation instructions prepended by the client.
Do not assume that the instruction the user selected has index zero. A composed
swap, mint, or payment can contain several setup and cleanup instructions.
2. Find the last active program in the logs
Solana logs show program entry with invoke [depth], followed by success or
failure. Read from the failure line upward to find the deepest program that did
not report success.
Program App111... invoke [1]
Program log: Instruction: Settle
Program Token111... invoke [2]
Program Token111... failed: custom program error: 0x1
Program App111... consumed 42871 of 200000 compute units
Program App111... failed: custom program error: 0x1771The inner token program failed first. The outer program then propagated or
translated that failure. Fixing only the outer error message would hide the
origin.
3. Reconstruct the CPI path
Invocation depth tells you which program called which. innerInstructions
adds the CPI instructions grouped by their parent top-level instruction index.
Use both fields when multiple programs share similar logs.
Capture the program IDs and account addresses for each hop. Then verify:
- the intended program ID was invoked
- writable and signer privileges reached the CPI correctly
- token accounts use the expected mint, owner, and token program
- PDA seeds and bump match the program's derivation
For the account model behind these checks, see
Solana accounts explained. For authority and
CPI validation before mainnet, use the
Solana program security guide.
4. Decode the program error
A raw custom program error: 0x1771 is hexadecimal. 0x1771 is decimal 6001.
That number only becomes meaningful when paired with the program that emitted
it and the matching program version or IDL.
Anchor custom user errors start at 6000. Anchor's client error can also expose
the error name, number, message, source location, compared values, logs, and
program stack. Prefer that structured error when available:
try {
await sendOperation();
} catch (error) {
const candidate = error as {
error?: {
errorCode?: { code?: string; number?: number };
errorMessage?: string;
origin?: { file?: string; line?: number };
};
logs?: string[];
};
console.error({
code: candidate.error?.errorCode,
message: candidate.error?.errorMessage,
origin: candidate.error?.origin,
logs: candidate.logs,
});
}Do not maintain one global map from number to message. Different programs may
use the same custom number. Key your decoder by program ID and deployed version.
5. Check state and compute evidence
If constraints and error decoding look correct, compare the inputs to the state
used by simulation. Record the simulation slot. Fetch important accounts at a
compatible commitment and inspect owners, balances, token amounts, authorities,
and application counters.
Then inspect unitsConsumed and the final compute log. A transaction that uses
nearly all of its limit in simulation has little margin for a different branch
or larger account data. Solana's RPC example reports use against a 200000-unit
limit. Compute exhaustion is an execution problem. Raising the limit can confirm
the diagnosis, but it does not remove inefficient work.
The output of this layer should be a concrete claim such as “instruction 2
entered program X, its token CPI failed because destination account Y belongs to
mint Z” rather than “the RPC rejected it.”
Match common symptoms to the correct fix
Use this map before changing code:
| Symptom | Likely layer | First check |
| --- | --- | --- |
| BlockhashNotFound | transaction context | blockhash age, commitment, RPC lag |
| InstructionError: [n, ...] | message instruction | instruction n and its accounts |
| Custom: 6000+ from Anchor | program rule | program IDL, error name, source origin |
| ConstraintSeeds | account derivation | seed bytes, ordering, program ID, bump |
| IncorrectProgramId | account or CPI target | account owner and supplied program ID |
| compute budget exceeded | execution cost | units consumed and expensive branch |
| simulation passes, send fails | state or transaction lifetime | simulation slot, blockhash, changed accounts |
| send returns a signature, nothing lands | delivery or confirmation | status, expiration, RPC forwarding |
The last row is not a program simulation problem. Follow the
Solana transaction reliability guide
for blockheight-aware confirmation and safe retries.
Preserve a debug envelope in production
Browser consoles disappear. Wallet adapters may replace the original error.
Production systems need a small, privacy-aware debug envelope for every failed
operation:
type SolanaDebugEnvelope = {
operationId: string;
cluster: string;
rpcHost: string;
simulationSlot?: number;
signature?: string;
failedInstructionIndex?: number;
programIds: string[];
error: unknown;
logs: string[];
unitsConsumed?: number;
blockhash?: string;
lastValidBlockHeight?: number;
};Redact private business data, never log secret keys, and avoid storing signed
transaction bytes unless your threat model and retention policy permit it. An
operation ID should connect the browser event, backend request, simulation,
submission attempt, and signature status without exposing wallet secrets.
When the trace shows inconsistent RPC context, delivery gaps, or missing
observability rather than a deterministic program error,
RPC Edge is the relevant boundary. RPC infrastructure
cannot fix program logic, but it can make simulation, submission, and status
evidence easier to compare.
The Solana Failure Reproduction Checklist
Before escalating a failed transaction, capture:
- cluster, RPC host, commitment, and simulation slot
- full err value and failed instruction index
- ordered program IDs and CPI depth from logs
- custom error in hexadecimal and decimal
- matching program IDL or deployed source version
- relevant account addresses, owners, mints, and authorities
- units consumed and configured compute limit
- blockhash, last valid blockheight, and signature if submitted
- whether the failure reproduces against the same account state
- the smallest transaction that still fails
This checklist turns a screenshot into a reproducible engineering case. It also
separates client composition, account state, program logic, and RPC delivery
before several teams investigate the same symptom.
FAQ
Why does simulation pass while the real transaction fails?
Simulation observes a specific bank state and does not reserve it. Accounts can
change before execution, the blockhash can expire, or the submitted message can
differ from the simulated one. Compare the serialized message, simulation slot,
account state, and transaction lifetime.
What does `custom program error: 0x1` mean?
It is a program error number written in hexadecimal. Decode it in the context of
the program that failed. The same number can mean different things for different
programs.
Should production apps show raw logs to users?
Usually no. Give users a stable operation reference and a clear next action.
Keep structured logs for support and engineering, with secrets and sensitive
business fields removed.
Can simulation replace program tests?
No. Simulation is valuable integration evidence against chain state. Unit,
property, and hostile-sequence tests cover branches and invariants that a single
simulation will not explore.
Where should I start when the error only says simulation failed?
Preserve the RPC response, inspect err, map the failed instruction index, then
read the logs from the deepest failed invocation outward. That is the shortest
path through the Five-Layer Debug Trace.
Primary sources
- Solana simulateTransaction RPC
- Solana RPC transaction error and inner-instruction structures
- Solana transaction confirmation and expiration
- Solana program limitations and compute budget
- Anchor custom errors
If a production failure crosses client composition, program behavior, and RPC
delivery, try the Builderz trace review.
We can isolate the failing layer before proposing a repair.



