Skip to main content
A transaction ends up in one of three places, and each needs a different fix. Find out which one before changing anything.

Step 1: find out what happened

Ask the cluster about the signature. searchTransactionHistory makes the lookup work for transactions of any age.
If you got an error back from sendTransaction itself, there is no signature to look up. The transaction was rejected before it was sent. Go to Rejected before sending. To check signatures from code, use the wait for confirmation recipe.

Rejected before sending

By default sendTransaction simulates the transaction first and refuses to send it if the simulation fails. You get error -32002 with the reason and the program logs:
Read it in this order:
  1. data.logs usually says what went wrong in plain words. Here, the payer has 0.06 SOL and tried to send 1,000.
  2. data.err says which instruction failed. InstructionError: [0, ...] is the first instruction, counting from zero.
  3. The custom error code belongs to the program that failed, not to Solana. Custom: 1 from the System Program is “insufficient funds”, but Custom: 1 from another program means something else. Look the number up in that program’s error list (its IDL, or its source). The message shows it in hex, so 0x1770 is 6000, the first custom error in an Anchor program.
A rejection costs nothing. The transaction never left the RPC node.

Common rejections

Debug with simulateTransaction

When you want the logs without sending anything, simulate. Two options make it far more useful while debugging:
  • replaceRecentBlockhash: true swaps in a current blockhash, so you can keep simulating the same transaction after its blockhash expires. It cannot be combined with sigVerify: true.
  • sigVerify: false skips signature checks, so you can simulate an unsigned or partially signed transaction.
The result has err, logs and unitsConsumed even when the simulation succeeds. Use unitsConsumed to set an accurate compute unit limit: a tight limit makes the priority fee you pay per unit go further.
Avoid skipPreflight: true while debugging. It sends the transaction without simulating, so a broken transaction lands, fails and charges the fee, and the error only appears later in getSignatureStatuses. Turn it on once the transaction is known to work and you need the speed.

Landed but failed

The transaction is on chain, the fee is gone, and none of its changes took effect. Get the full record:
meta.err has the same shape as a simulation error and meta.logMessages has the program logs. Read them as described in Rejected before sending. A transaction that simulated fine can still fail on chain because state changed in between. The usual culprits are slippage on a swap, an account someone else closed or modified, or a nonce that was already used. Re-simulate against current state to confirm, then build in tolerance: wider slippage, a fresh quote, or a retry that rebuilds the transaction.

Never landed

getSignatureStatuses returns null: the transaction was accepted by the RPC node but no leader included it. Nothing was charged.

The blockhash expired

A transaction is only valid for 150 blocks after its blockhash. With slots taking about 215 ms, that is roughly 30 to 35 seconds. Once it passes, the transaction can never land, so stop waiting and rebuild it. Check whether a blockhash is still usable:
Save the lastValidBlockHeight that getLatestBlockhash returns alongside your blockhash. Compare it with getBlockHeight: while the block height is lower, keep resending the same signed transaction. Once it is higher, the transaction is dead.

The priority fee was too low

When blocks are busy, leaders fill them with the transactions that pay most per compute unit. Check what recently landed for the accounts you touch:
Pick a value from the recent results, set it with SetComputeUnitPrice, and pair it with an accurate SetComputeUnitLimit from simulation.

It was dropped on the way

A sent transaction can be lost between the RPC node and the leader, especially during congestion. Resend the same signed bytes every couple of seconds until it confirms or the blockhash expires. Resending is safe: the same signature can only land once. For transactions where landing quickly matters, send through Apex. Apex sends each transaction to upcoming leaders over several paths at once and keeps retrying until it lands or expires. Its own errors are listed in Apex errors and rate limits.

Still stuck

Collect the signature (or the base64 transaction if it never got one), the full error including data.logs, the region you sent through, and whether preflight was on. Then ask in Discord.