Skip to main content
Protochain returns structured errors that tell you whether to retry, rebuild, or check on-chain state. The patterns below cover the most common scenarios.
For the full list of error codes with retryability classifications and remediation actions, see the Error Reference.

Pattern 1: Checking if an error is retryable

Inspect the TransactionErrorCode on a SubmitTransaction error to determine whether you can retry the same transaction, need to rebuild it, or need to check on-chain state. Error codes fall into three categories (see Error Reference):
  • Temporary (INSUFFICIENT_FUNDS, ACCOUNT_IN_USE, WOULD_EXCEED_BLOCK_LIMIT) — transaction was NOT sent; retry the same transaction
  • Permanent (INVALID_TRANSACTION, INVALID_SIGNATURE, PROGRAM_ERROR, etc.) — transaction was NOT sent; do not retry without rebuilding
  • Indeterminate (TIMEOUT, NETWORK_ERROR, RATE_LIMITED, etc.) — unknown whether the transaction was sent; check certainty (see Pattern 2)
resp, err := client.SubmitTransaction(ctx, &transaction_v1.SubmitTransactionRequest{
    CompiledTransaction: compiledTx,
})
if err != nil {
    st, ok := status.FromError(err)
    if !ok {
        return fmt.Errorf("non-gRPC error: %w", err)
    }
    for _, detail := range st.Details() {
        if txErr, ok := detail.(*transaction_v1.TransactionError); ok {
            switch txErr.Code {
            case transaction_v1.TransactionErrorCode_TRANSACTION_ERROR_CODE_INSUFFICIENT_FUNDS,
                transaction_v1.TransactionErrorCode_TRANSACTION_ERROR_CODE_ACCOUNT_IN_USE,
                transaction_v1.TransactionErrorCode_TRANSACTION_ERROR_CODE_WOULD_EXCEED_BLOCK_LIMIT:
                // Temporary — retry the same transaction after a short wait
                time.Sleep(500 * time.Millisecond)
                return retrySubmit(ctx, client, compiledTx)
            case transaction_v1.TransactionErrorCode_TRANSACTION_ERROR_CODE_TIMEOUT,
                transaction_v1.TransactionErrorCode_TRANSACTION_ERROR_CODE_NETWORK_ERROR,
                transaction_v1.TransactionErrorCode_TRANSACTION_ERROR_CODE_RATE_LIMITED:
                // Indeterminate — check certainty before deciding
                return handleIndeterminate(ctx, client, txErr)
            default:
                // Permanent — do not retry without rebuilding
                return fmt.Errorf("permanent error: %s", txErr.Code)
            }
        }
    }
}

Pattern 2: Handling indeterminate submission (TransactionSubmissionCertainty)

Indeterminate errors are the trickiest case — you don’t know if the transaction was broadcast. The certainty field on the TransactionError tells you how to proceed.
  • CERTAINTY_SUBMITTED — the transaction was sent; use GetTransaction or MonitorTransaction to poll for the result
  • CERTAINTY_NOT_SUBMITTED — the transaction was not sent; safe to retry or resubmit
  • CERTAINTY_UNKNOWN_RESOLVABLE — uncertain but resolvable; wait until after blockhash_expiry_slot, then query on-chain — if not found, safe to recompile and resubmit
  • CERTAINTY_UNKNOWN — uncertain and not easily resolvable; treat conservatively, wait for blockhash expiry, query on-chain before deciding to resubmit
func handleIndeterminate(ctx context.Context, client transaction_v1.TransactionServiceClient, txErr *transaction_v1.TransactionError) error {
    switch txErr.Certainty {
    case transaction_v1.TransactionSubmissionCertainty_TRANSACTION_SUBMISSION_CERTAINTY_SUBMITTED:
        // Transaction was sent — monitor for on-chain result
        log.Println("Transaction sent. Monitoring for confirmation...")
        return monitorTransaction(ctx, client, txErr.Signature)

    case transaction_v1.TransactionSubmissionCertainty_TRANSACTION_SUBMISSION_CERTAINTY_NOT_SUBMITTED:
        // Transaction was NOT sent — safe to retry immediately
        log.Println("Transaction not sent. Retrying...")
        return retrySubmit(ctx, client, compiledTx)

    case transaction_v1.TransactionSubmissionCertainty_TRANSACTION_SUBMISSION_CERTAINTY_UNKNOWN_RESOLVABLE:
        // Wait for blockhash expiry, then check on-chain
        log.Printf("Uncertain. Waiting for blockhash expiry at slot %d...", txErr.BlockhashExpirySlot)
        waitForSlot(ctx, txErr.BlockhashExpirySlot)
        found, err := checkOnChain(ctx, client, txErr.Signature)
        if err != nil {
            return err
        }
        if !found {
            // Safe to recompile and resubmit
            return recompileAndSubmit(ctx, client, instructions)
        }
        return nil

    default: // CERTAINTY_UNKNOWN or CERTAINTY_UNSPECIFIED
        // Treat conservatively — wait, query, then decide
        log.Println("Certainty unknown. Waiting for blockhash expiry before checking on-chain.")
        waitForSlot(ctx, txErr.BlockhashExpirySlot)
        found, _ := checkOnChain(ctx, client, txErr.Signature)
        if !found {
            return recompileAndSubmit(ctx, client, instructions)
        }
        return nil
    }
}

Pattern 3: Handling program execution failures

TRANSACTION_ERROR_CODE_PROGRAM_ERROR means the on-chain program rejected the instruction. The error code itself is generic — call GetTransaction with the transaction signature to retrieve the detailed meta_error_message from the on-chain logs.
if txErr.Code == transaction_v1.TransactionErrorCode_TRANSACTION_ERROR_CODE_PROGRAM_ERROR {
    // Fetch detailed error message from on-chain logs
    txResp, err := client.GetTransaction(ctx, &transaction_v1.GetTransactionRequest{
        Signature: txErr.Signature,
    })
    if err != nil {
        return fmt.Errorf("failed to fetch transaction details: %w", err)
    }
    log.Printf("Program error: %s", txResp.Transaction.MetaErrorMessage)
    return fmt.Errorf("program rejected instruction: %s", txResp.Transaction.MetaErrorMessage)
}

Pattern 4: Handling expired blockhash

Compiled transactions expire after approximately 150 slots (~60 seconds). TRANSACTION_ERROR_CODE_BLOCKHASH_EXPIRED means the transaction is stale and cannot be submitted. You must recompile — not just re-sign — to get a fresh blockhash.
if txErr.Code == transaction_v1.TransactionErrorCode_TRANSACTION_ERROR_CODE_BLOCKHASH_EXPIRED {
    // Recompile to get a fresh blockhash — do not just retry with the same compiled transaction
    compileResp, err := txClient.CompileTransaction(ctx, &transaction_v1.CompileTransactionRequest{
        Instructions: instructions,
        FeePayer:     feePayer,
    })
    if err != nil {
        return fmt.Errorf("recompile failed: %w", err)
    }
    // Re-sign and re-submit with the freshly compiled transaction
    return signAndSubmit(ctx, txClient, compileResp.CompiledTransaction)
}