Logo
New RPC users get 35% off their first monthView the offer
OnFinality Learn
RPC Troubleshooting12 min read

Decoding Solana simulateTransaction Errors: Instruction Index, Program ID, and Custom Error Codes

Learn to read and fix Solana simulateTransaction preflight errors: decode instruction index, program ID, and custom error codes from JSON-RPC logs.

TL;DR

When a Solana transaction fails preflight simulation, the JSON-RPC error message 'Transaction simulation failed' contains a logs array that pinpoints the failure. The key is to read the line 'Program <PROGRAM_ID> failed: custom program error: 0x<N>' or 'Error processing Instruction N: Program failed to complete', then map the instruction index to the transaction's compiled instructions and the numeric code to the program's error enum. This article explains the mechanism, provides a reproducible Node.js script to parse the error, and gives a checklist to fix common causes.

Direct Answer: How to Read a Solana Simulation Error

When you call simulateTransaction (or sendTransaction with preflight enabled) on a Solana RPC endpoint, the node runs the transaction against the current bank state. If it fails, the JSON-RPC response is an error object with message: "Transaction simulation failed" and a data.logs array. The last meaningful log line usually tells you exactly what went wrong. For a program-coded error, you'll see Program <PROGRAM_ID> failed: custom program error: 0x<N>. For a runtime-level failure (e.g., an instruction invoked a program that couldn't complete), you'll see Error processing Instruction N: Program failed to complete. The instruction index N refers to the position in the transaction's compiled instructions (after any address-table lookups for versioned transactions). The hex code 0x<N> is the index of the error variant in the failing program's custom error enum, starting at 0x0 for the first variant. To fix the issue, you must map that index back to the program's source code and check the state of the accounts involved.

This article is part of the OnFinality Learn hub and focuses on diagnosing failed transactions, complementing our guides on Solana versioned transactions and parsing and Solana RPC timeouts and retries. If you're new to Solana RPC endpoints, see our Solana JSON-RPC methods (RPC Assistant) and the Solana network overview.

Mechanism: What Happens During Preflight Simulation

The Solana runtime processes a transaction in a deterministic sequence. First, it checks the fee payer's balance and the blockhash. Then it loads each account referenced by the transaction. For each instruction, it invokes the specified program with the given accounts and instruction data. If any step fails, the runtime aborts and returns an error. The RPC node captures the logs emitted during this process and attaches them to the JSON-RPC error response.

The simulateTransaction RPC method (documented in the Solana RPC documentation) accepts a sigVerify flag and an optional accounts configuration. By default, it does not require a recent blockhash unless you set replaceRecentBlockhash. The response includes a value object with err, logs, and accounts. When err is non-null, the transaction failed. The logs array contains messages from the runtime and from the programs themselves. The final error line is typically one of two forms:

  1. Program <PROGRAM_ID> failed: custom program error: 0x<N> – the program returned a custom error. The hex number is the index of the error variant in the program's error enum. For example, if a program defines enum MyError { InsufficientFunds, InvalidOwner }, then 0x0 means InsufficientFunds and 0x1 means InvalidOwner.

  1. Error processing Instruction N: Program failed to complete – the program did not return a clean error (e.g., it panicked, ran out of compute units, or hit an unexpected syscall failure). The instruction index N tells you which instruction caused the problem.

Other common runtime errors include BlockhashNotFound (the blockhash is stale or invalid) and AccountInUse (a conflict with another transaction). These are not program-specific and usually indicate a client-side issue rather than a logic bug.

  • The preflight simulation is optional: sendTransaction accepts skipPreflight: true to bypass it, but the transaction will still fail on submission if it's invalid.
  • The logs array may contain partial logs before the error line; those can help you understand how far the transaction got.
  • For versioned transactions (v0), the instruction index in the error refers to the order in the compiled instructions array, which may include address-table lookups. The SDK's Transaction object exposes instructions in the same order.

Decoding the Instruction Index and Program ID

The instruction index N in Error processing Instruction N is zero-based. To find the corresponding instruction, look at the transaction's instructions array (for legacy) or message.compiledInstructions (for versioned). Each instruction has a programIdIndex that points into the account keys array. The program ID is the account key at that index. If you're using @solana/web3.js, the Transaction object has a compileMessage() method that returns the compiled instructions. For versioned transactions, the TransactionMessage class can decompile the message back into TransactionInstruction objects.

Once you have the program ID, you can identify which program failed. If it's a well-known program (e.g., the System Program, Token Program, or Associated Token Account Program), the error codes are documented in the Solana source code. For custom programs, you need to look at the program's source to map the error code. The convention is that the error enum's variant index matches the hex code. For example, if the log says custom program error: 0x2, the program returned the third variant (index 2) of its error enum.

It's important to distinguish between a program error and a runtime error. A program error is returned by the program itself via ProgramError::Custom(u32). A runtime error like ProgramFailedToComplete means the program execution was aborted due to an unexpected condition, such as a panic or exceeding the compute budget. In that case, the logs may show a Program log: Panicked line or a compute-unit exhaustion message.

  • Check the programIdIndex of the instruction to get the program ID from the account keys.
  • For versioned transactions, remember that the compiled instructions may reference accounts from address lookup tables; the SDK handles this transparently when you use TransactionMessage.decompile().
  • If the error is Program failed to complete, look for a preceding Program log: Panicked or Program log: Error: line to get more details.

Reproducible Example: Parsing a Simulation Error with Node.js

The following script uses @solana/web3.js to build a simple transaction, simulate it against a user-supplied RPC URL, and print the raw JSON-RPC error plus a parsed breakdown. Replace YOUR_RPC_URL with your endpoint (e.g., from OnFinality's Solana RPC). The script intentionally creates a transaction that will fail (e.g., transferring lamports from an account with no balance) to demonstrate the error format.

Run it with Node.js 18+ and npm install @solana/web3.js. The script prints the raw error object and then extracts the instruction index, program ID, and error code using a simple regex on the logs.

  • Expected output: The simulation result will contain err with a message like "Transaction simulation failed: Error processing Instruction 0: custom program error: 0x0" and logs showing Program 11111111111111111111111111111111 failed: custom program error: 0x0 (the System Program's InsufficientFunds error).
  • The script prints the raw JSON-RPC error and the parsed fields. Use it as a template for your own transactions.
const { Connection, Keypair, SystemProgram, Transaction, LAMPORTS_PER_SOL } = require('@solana/web3.js');

const RPC_URL = process.env.RPC_URL || 'YOUR_RPC_URL';
const connection = new Connection(RPC_URL, 'confirmed');

async function main() {
  // Create a fee payer with no lamports (will cause simulation to fail)
  const feePayer = Keypair.generate();
  const recipient = Keypair.generate();

  const tx = new Transaction().add(
    SystemProgram.transfer({
      fromPubkey: feePayer.publicKey,
      toPubkey: recipient.publicKey,
      lamports: LAMPORTS_PER_SOL,
    })
  );
  tx.feePayer = feePayer.publicKey;
  tx.recentBlockhash = (await connection.getLatestBlockhash()).blockhash;

  try {
    const result = await connection.simulateTransaction(tx);
    console.log('Simulation result:', JSON.stringify(result, null, 2));
    if (result.value.err) {
      const logs = result.value.logs || [];
      const errorLine = logs.find(l => l.includes('failed:') || l.includes('Error processing'));
      console.log('\nParsed error line:', errorLine);
      // Extract instruction index and program ID
      const match = errorLine.match(/Error processing Instruction (\d+): Program (\S+) failed/);
      if (match) {
        console.log('Instruction index:', match[1]);
        console.log('Program ID:', match[2]);
      }
      const customMatch = errorLine.match(/custom program error: (0x[0-9a-fA-F]+)/);
      if (customMatch) {
        console.log('Custom error code:', customMatch[1]);
      }
    }
  } catch (e) {
    console.error('RPC error:', e.message);
    if (e.data && e.data.logs) {
      console.log('Logs:', e.data.logs);
    }
  }
}

main().catch(console.error);

Results Table: Fill in for Your Own Transaction

When you run the script against your own failing transaction, record the following fields. This table helps you systematically diagnose the issue.

  • FieldValue (fill in)Interpretation
    RPC URLThe endpoint used
    Transaction typeLegacy / VersionedAffects instruction indexing
    Error messagee.g., 'Transaction simulation failed'
    Instruction indexWhich instruction failed (0-based)
    Program IDThe program that returned the error
    Custom error codeHex code like 0x0
    Error enum variantMap code to program's error enum
    Fee payer balanceCheck if sufficient
    BlockhashCheck if recent
    Logs before errorPartial logs for context

Failure and Fix Checklist

Use this checklist to resolve common simulation failures. The first step is always to identify whether the error is from the runtime (e.g., insufficient fee-payer balance) or from a program (custom error).

Fee-payer issues: If the error is 0x0 from the System Program, it usually means the fee payer has no lamports to cover the transaction fee or the transfer amount. Check the fee payer's balance with getBalance. Also ensure the blockhash is recent; a stale blockhash causes BlockhashNotFound.

Account not initialized: If a program expects an account to be initialized (e.g., a token account), the simulation may fail with a custom error like UninitializedAccount. Verify that all required accounts exist and are owned by the correct program.

Program custom error: Map the hex code to the program's error enum. For example, if the program has enum MyError { InvalidOwner, InsufficientFunds }, then 0x0 is InvalidOwner and 0x1 is InsufficientFunds. Look at the program's source code or ABI to understand the condition.

Instruction ordering: For versioned transactions, ensure you're using the correct instruction index. The SDK's Transaction object handles this, but if you're manually constructing the message, double-check the order of compiled instructions.

Compute unit exhaustion: If the logs show Program failed to complete and a compute unit message, increase the compute budget by adding a ComputeBudgetProgram.setComputeUnitLimit instruction.

Provider-specific preflight settings: Some RPC providers may have different preflight behaviors (e.g., they might skip preflight by default or use a different commitment level). Check your provider's documentation. OnFinality's RPC pricing and API service pages describe our standard settings, but always verify with your endpoint.

Limitations and Tradeoffs

Simulation is not a guarantee that the transaction will succeed when submitted. The state may change between simulation and confirmation, leading to a different outcome. Also, simulation does not execute the transaction against the latest state if you use a lower commitment level; use commitment: 'confirmed' or 'finalized' for more accurate results.

The logs array may be truncated for very large transactions or when the program logs too much. In such cases, the error line might be missing. You can increase the log limit by setting the encoding parameter, but this is not always supported.

For versioned transactions, the instruction index in the error refers to the compiled instruction order, which may differ from the order you constructed if address table lookups are involved. Always decompile the message to verify.

Provider-specific preflight settings can affect whether you see the error at all. Some providers may return a generic error without logs. If you encounter this, try using a different RPC endpoint or the simulateTransaction method directly.

Next Steps and Further Reading

Now that you can decode simulation errors, you can apply this knowledge to debug your Solana applications. For more advanced topics, explore our other guides:

For official protocol details, refer to the Solana documentation on simulateTransaction and the Solana error codes reference. The Solana Cookbook also has community-contributed error explanations.

Never Worry about Infrastructure Again

OnFinality takes away the heavy lifting of DevOps so you can build smarter and faster.

Get Started