After a transaction is sent, getTransaction returns a meta object that carries the authoritative post-execution result: err, logMessages, innerInstructions, balance deltas, and compute usage. The err field can be a plain string or an object such as { InstructionError: [index, reason] }, where index points at the outer instruction in the message, not at an inner instruction. innerInstructions[].index uses the same outer-instruction indexing, so you must fold each inner list back onto its parent outer instruction before you can name the failing program. This article walks the meta fields, the index-correlation rule, the err shapes, and a runnable @solana/web3.js decoder that prints a correlated instruction tree. It also covers the failure modes that produce null results or misleading errors, and how to measure behavior against your own endpoint.
Why getTransaction Is the Post-Send Decoding Method
getTransaction is the Solana JSON-RPC method that returns a confirmed transaction by signature, including its meta object. It is the correct tool once a transaction has been submitted and you need the authoritative execution result. The sibling article on Decoding Solana simulateTransaction errors covers pre-send simulation, where the node executes against current state without committing; getTransaction instead reports what actually happened on chain.
The request shape matters. A minimal call passes the signature and a configuration object with encoding, commitment, and maxSupportedTransactionVersion. The encoding controls how instructions and account keys are returned; jsonParsed is convenient for standard programs, while json or base64 preserves raw compiled data. The commitment level determines which ledger state the node reads from, and the semantics of processed, confirmed, and finalized are covered in Solana commitment levels: processed vs confirmed vs finalized.
Omitting maxSupportedTransactionVersion is a common cause of failure on versioned (v0) transactions. The Solana RPC documentation for getTransaction states that the field is required when the transaction uses a versioned message; without it, the node cannot know which message format to decode and returns an error rather than a result. Always set it to 0 unless you have a specific reason to request a different supported version.
- Use getTransaction after submission; use simulateTransaction before submission.
- Set encoding to jsonParsed for readable instructions, or base64 for raw fidelity.
- Set commitment explicitly so you know which ledger state you are reading.
- Set maxSupportedTransactionVersion to 0 for v0 transactions.
curl -s https://api.mainnet-beta.solana.com -X POST -H 'Content-Type: application/json' -d '{
"jsonrpc": "2.0",
"id": 1,
"method": "getTransaction",
"params": [
"YOUR_SIGNATURE_HERE",
{ "encoding": "jsonParsed", "commitment": "confirmed", "maxSupportedTransactionVersion": 0 }
]
}'The meta Object Field by Field
The meta object is the post-execution summary. The Solana RPC documentation for getTransaction defines its fields, and each one answers a different question. err tells you whether execution failed and, if so, how. status is a newer confirmation summary that can carry Ok or Err. fee reports the lamports charged. preBalances and postBalances give the lamport delta per account index, while preTokenBalances and postTokenBalances do the same for SPL token accounts.
logMessages is the ordered program log stream, including lines such as 'Program log:' and 'Program ... failed'. loadedAddresses lists the address-table accounts loaded for versioned transactions, which matters because instruction account indices can refer to them. computeUnitsConsumed reports the compute budget used. innerInstructions contains the cross-program invocations (CPIs) triggered by outer instructions. rewards lists staking or voting rewards when the transaction touches those paths.
No single field is sufficient. err names the failure but not always the program; logMessages gives narrative context but can be truncated; innerInstructions gives structure but requires index correlation. Treat meta as a set of corroborating signals and reconcile them before you present a cause to a user.
- err: failure indicator, string or object form.
- status: confirmation summary, Ok or Err.
- fee: lamports charged for the transaction.
- preBalances / postBalances: lamport deltas by account index.
- preTokenBalances / postTokenBalances: token deltas by token account.
- logMessages: ordered program log lines.
- loadedAddresses: address-table accounts for versioned transactions.
- computeUnitsConsumed: compute budget used.
- innerInstructions: CPIs grouped by outer instruction index.
- rewards: staking or voting rewards when applicable.
The Index-Correlation Rule for innerInstructions
The most important decoding rule is that innerInstructions[].index refers to the position of the outer instruction in the transaction's message, not to an inner instruction. Each entry groups the inner instructions that ran because of that outer instruction. The Solana RPC documentation for InnerInstruction and CompiledInstruction establishes this indexing. To attribute a failure, you must fold each inner list back onto its parent outer instruction.
A practical way to think about it: the outer instruction at message index i owns the inner instructions listed under innerInstructions where index equals i. If a wallet program appears as an inner instruction, it was invoked by the outer instruction at that index, not by the transaction's first instruction by default. This is why naive parsers that read innerInstructions in order and assume they belong to the first instruction misattribute failures.
When you build a correlated tree, walk the message instructions in order, attach the matching inner list to each, and then search the combined set for the failing program id. The failing program is the one named in the 'Program ... failed' log line and, when present, in the InstructionError index. Cross-check both before you tell a user which program rejected the transaction.
- innerInstructions[].index is an outer-instruction index in the message.
- Inner instructions run in the range [index, next index).
- Fold inner lists onto their parent outer instruction before attribution.
- Reconcile the failing program id from logs and from the InstructionError index.
err Object Shapes and How to Present Them
The err field has two broad forms. The string form appears for transaction-level failures such as 'AccountInUse' or 'BlockhashNotFound'. These are not tied to a specific instruction and usually indicate a submission or state condition rather than a program rejection. The object form appears for instruction-level failures, most commonly { InstructionError: [index, reason] }, where index is the outer instruction position and reason describes the failure.
The reason can itself be nested. A program-defined failure often appears as { Custom: n }, where n is a program-specific error code. The Solana RPC documentation for TransactionError enumerates the standard variants, and the program's own documentation maps Custom codes to meanings. When you present this to a user, translate the outer index into the instruction's program id using the message, then translate the Custom code using the program's error table.
Do not conflate submission-time JSON-RPC errors with meta.err. Errors returned at submission, such as -32002 Transaction simulation failed, -32003 Transaction signature verification failure, and -32005 node is behind, follow the JSON-RPC 2.0 error-object envelope with code, message, and data, as defined in the JSON-RPC 2.0 Specification. Those are transport-level or pre-acceptance errors; meta.err is the on-chain result.
- String form: transaction-level condition, not instruction-specific.
- Object form: { InstructionError: [index, reason] } for instruction failures.
- Nested form: { Custom: n } for program-defined error codes.
- Submission errors use the JSON-RPC 2.0 code/message/data envelope.
Runnable Node.js Decoder with @solana/web3.js
The following example fetches a transaction by signature, reads meta.err, walks meta.logMessages for failure lines, and prints a correlated instruction tree that names the failing program. It uses @solana/web3.js and assumes a configured RPC endpoint. Replace the endpoint and signature with your own values.
The decoder folds innerInstructions onto their parent outer instruction using the index rule, then searches the combined set for the program id named in the failure log. It prints the outer instruction index, the program id, and any inner instructions beneath it. This gives you an attributable failure rather than a raw error string.
const { Connection, PublicKey } = require('@solana/web3.js');
async function decodeTransaction(endpoint, signature) {
const connection = new Connection(endpoint, 'confirmed');
const tx = await connection.getTransaction(signature, {
commitment: 'confirmed',
maxSupportedTransactionVersion: 0,
});
if (!tx) {
console.log('No transaction found for signature:', signature);
return;
}
const meta = tx.meta;
console.log('err:', JSON.stringify(meta.err));
console.log('computeUnitsConsumed:', meta.computeUnitsConsumed);
const message = tx.transaction.message;
const accountKeys = message.staticAccountKeys || message.accountKeys;
const outerInstructions = message.compiledInstructions || message.instructions;
const innerByIndex = new Map();
for (const group of meta.innerInstructions || []) {
innerByIndex.set(group.index, group.instructions);
}
const failingPrograms = new Set();
for (const line of meta.logMessages || []) {
const failed = line.match(/Program (\S+) failed/);
if (failed) failingPrograms.add(failed[1]);
}
outerInstructions.forEach((ix, outerIndex) => {
const programId = accountKeys[ix.programIdIndex].toString();
const inner = innerByIndex.get(outerIndex) || [];
const isFailing = failingPrograms.has(programId);
console.log(
`outer[${outerIndex}] program=${programId}${isFailing ? ' <-- FAILED' : ''}`
);
inner.forEach((innerIx, innerIndex) => {
const innerProgramId = accountKeys[innerIx.programIdIndex].toString();
const innerFailing = failingPrograms.has(innerProgramId);
console.log(
` inner[${outerIndex}.${innerIndex}] program=${innerProgramId}${innerFailing ? ' <-- FAILED' : ''}`
);
});
});
if (meta.err && meta.err.InstructionError) {
const [index, reason] = meta.err.InstructionError;
const programId = accountKeys[outerInstructions[index].programIdIndex].toString();
console.log(`InstructionError at outer[${index}] program=${programId} reason=${JSON.stringify(reason)}`);
}
}
decodeTransaction('YOUR_RPC_ENDPOINT', 'YOUR_SIGNATURE');Correlating Logs, Inner Instructions, and Program IDs
Log correlation is the bridge between the raw err object and a human explanation. The 'Program log:' lines show what a program printed, and the 'Program ... failed' line names the program that aborted. Because inner instructions are grouped by outer index, you can walk the log stream and the instruction tree together to see which outer instruction triggered the failing CPI.
A reliable procedure is to first locate the failing program id from the log line, then find every occurrence of that program id in the correlated tree, then check whether the InstructionError index points at the outer instruction that owns it. If the failing program appears only as an inner instruction, the outer instruction at that index is the entry point that invoked it. This distinction matters when a wallet or aggregator program calls a token program that rejects the transfer.
The Solana Cookbook and the @solana/web3.js source document the client-side decoding path for inner instructions, log messages, and program ids by index. Use them as the reference for field names and for the compiled instruction layout, especially when you switch between jsonParsed and base64 encodings.
- Find the failing program id from the 'Program ... failed' log line.
- Locate that program id in the correlated instruction tree.
- Check whether the InstructionError index points at the owning outer instruction.
- Distinguish an outer entry point from an inner CPI when attributing blame.
Results Table: Measure Against Your Own Endpoint
Provider behavior varies, so measure against your own endpoint rather than relying on general claims. The table below is a template: fill each row with the observed result from your RPC provider. Do not treat any row as a fixed expectation, because documented behavior varies by provider and by commitment level.
Run the same signature through each endpoint you use and record the outcome. This surfaces differences in log truncation, null handling, and versioned-transaction support before they affect production.
- Endpoint: the RPC URL you tested.
- Commitment: the level you requested.
- Result: transaction object or null.
- meta.err: the observed err value.
- logMessages count: number of log lines returned.
- innerInstructions groups: number of groups returned.
- maxSupportedTransactionVersion: whether the call succeeded without it.
- Notes: any truncation or provider-specific behavior observed.
Failure Modes: Null Results, Commitment, and Truncation
A null result is the most common surprise. getTransaction returns null when the signature is not found at the requested commitment level, which often means the transaction is not yet confirmed or the node has not caught up. If you requested finalized but the transaction is only confirmed, you may see null until finalization. Retry with a lower commitment or wait, and consider the retry guidance in Solana RPC timeouts and retry strategy.
Omitting maxSupportedTransactionVersion fails on v0 transactions, as noted earlier. Commitment mismatch produces null or stale data rather than an explicit error, so always log the commitment you used. Some providers truncate logMessages, which can hide the 'Program ... failed' line; if logs look short, cross-check with a second endpoint or with a block explorer.
Signature lookup itself can be paginated when you are scanning history rather than fetching one transaction. If you are enumerating signatures first, see Solana getSignaturesForAddress pagination for the cursor pattern, then feed each signature into getTransaction.
- Null result: not found at the requested commitment, or node lag.
- Commitment mismatch: null or stale data without an explicit error.
- Missing maxSupportedTransactionVersion: failure on v0 transactions.
- Truncated logs: provider-specific, can hide the failing program line.
Limitations and Tradeoffs
getTransaction gives you the on-chain result, but it does not explain intent. A Custom error code is only meaningful with the program's error table, and a truncated log stream can leave the failure ambiguous. The meta object also reflects the state at the commitment you requested, so a processed read can differ from a finalized read. There is no single field that names the failing program in all cases; attribution requires reconciling err, logs, and inner instructions.
Performance and availability tradeoffs are provider-dependent. Higher commitment levels are safer but slower to return, and some providers limit historical lookups or log retention. For production systems, cache decoded results by signature and store the commitment level alongside them so you can reproduce the exact view later. The Solana RPC API guide (RPC Assistant) covers endpoint selection and method coverage, and RPC pricing describes plan-level differences.
- Custom codes require the program's own error table.
- Truncated logs can make attribution ambiguous.
- Commitment level changes what the meta object reflects.
- Cache decoded results with the commitment level for reproducibility.
Troubleshooting Checklist for Stubborn Transactions
When a transaction will not decode cleanly, work through the checklist in order. First confirm the signature is correct and the transaction is confirmed at your requested commitment. Second, add maxSupportedTransactionVersion if it is missing. Third, compare logMessages length across two endpoints to detect truncation. Fourth, verify that your inner-instruction folding uses the outer index, not the inner order.
If the err object is a string such as 'BlockhashNotFound', the transaction likely never landed; treat it as a submission issue rather than a program failure. If the err is { InstructionError: [index, reason] } with a nested { Custom: n }, resolve the program id at that outer index and look up n in the program's documentation. For versioned transactions, remember that account indices may refer to loadedAddresses, which is covered in Solana versioned transactions and getBlock parsing.
- Confirm signature and commitment before decoding.
- Add maxSupportedTransactionVersion for v0 transactions.
- Compare log lengths across endpoints to detect truncation.
- Fold inner instructions by outer index, not by inner order.
- Resolve Custom codes against the program's error table.