Published 12/4/2025 · 7 min read
Tags: solana , javascript , web3 , errors , transactions
Solana transactions fail. Networks are unreliable. Users reject signatures. Let’s build robust error handling.
Common Error Types
1. User Rejection
User clicked “Reject” in their wallet:
try {
const signedTx = await wallet.signTransaction(tx);
} catch (err) {
if (err.message.includes("User rejected")) {
// Don't retry - user said no
return { error: "Transaction cancelled" };
}
throw err;
}
2. Insufficient Funds
Not enough SOL for fees or not enough tokens:
try {
await sendTransaction(tx);
} catch (err) {
if (
err.message.includes("insufficient funds") ||
err.message.includes("Insufficient")
) {
return { error: "Insufficient balance" };
}
throw err;
}
3. Blockhash Expired
Transaction took too long:
try {
await sendTransaction(tx);
} catch (err) {
if (
err.message.includes("block height exceeded") ||
err.message.includes("Blockhash not found")
) {
// Rebuild with fresh blockhash and retry
return await retryWithFreshBlockhash();
}
throw err;
}
4. Network Errors
RPC node unreachable:
try {
await connection.getLatestBlockhash();
} catch (err) {
if (
err.message.includes("fetch failed") ||
err.message.includes("NetworkError")
) {
return { error: "Network unavailable. Please try again." };
}
throw err;
}
A Robust Transaction Function
Here’s a pattern for reliable transaction sending:
// src/lib/utils/transactions.ts
import {
Transaction,
Connection,
TransactionSignature,
Commitment,
} from "@solana/web3.js";
interface TransactionResult {
success: boolean;
signature?: TransactionSignature;
error?: string;
}
export async function sendAndConfirmWithRetry(
connection: Connection,
transaction: Transaction,
signTransaction: (tx: Transaction) => Promise<Transaction>,
options: {
maxRetries?: number;
commitment?: Commitment;
} = {}
): Promise<TransactionResult> {
const { maxRetries = 3, commitment = "confirmed" } = options;
let lastError: Error | null = null;
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
// Always get fresh blockhash
const { blockhash, lastValidBlockHeight } =
await connection.getLatestBlockhash(commitment);
transaction.recentBlockhash = blockhash;
// Sign
const signedTx = await signTransaction(transaction);
// Send
const signature = await connection.sendRawTransaction(
signedTx.serialize(),
{
skipPreflight: false,
preflightCommitment: commitment,
}
);
// Confirm with timeout
const confirmation = await connection.confirmTransaction(
{
signature,
blockhash,
lastValidBlockHeight,
},
commitment
);
if (confirmation.value.err) {
throw new Error(
`Transaction failed: ${JSON.stringify(confirmation.value.err)}`
);
}
return { success: true, signature };
} catch (err) {
lastError = err as Error;
// Don't retry user rejections
if (lastError.message.includes("User rejected")) {
return { success: false, error: "Transaction cancelled" };
}
// Don't retry insufficient funds
if (lastError.message.includes("insufficient")) {
return { success: false, error: "Insufficient balance" };
}
// Retry on blockhash/network errors
if (attempt < maxRetries - 1) {
console.log(`Attempt ${attempt + 1} failed, retrying...`);
await sleep(1000 * (attempt + 1)); // Exponential backoff
continue;
}
}
}
return {
success: false,
error: lastError?.message || "Transaction failed",
};
}
function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
Simulating Before Sending
Always simulate to catch errors before bothering the user:
async function simulateTransaction(
connection: Connection,
transaction: Transaction
): Promise<{ success: boolean; error?: string; logs?: string[] }> {
try {
const simulation = await connection.simulateTransaction(transaction);
if (simulation.value.err) {
// Parse the error
const errorMessage = parseSimulationError(simulation.value.err);
return {
success: false,
error: errorMessage,
logs: simulation.value.logs ?? undefined,
};
}
return {
success: true,
logs: simulation.value.logs ?? undefined,
};
} catch (err) {
return {
success: false,
error: (err as Error).message,
};
}
}
function parseSimulationError(err: any): string {
if (typeof err === "string") return err;
// Common Solana program errors
if (err.InstructionError) {
const [index, instructionError] = err.InstructionError;
if (typeof instructionError === "object") {
if (instructionError.Custom !== undefined) {
return `Program error ${instructionError.Custom} at instruction ${index}`;
}
}
return `Instruction ${index} failed: ${JSON.stringify(instructionError)}`;
}
return JSON.stringify(err);
}
Error Display Component
A Svelte component for user-friendly error display:
<!-- src/lib/components/TransactionStatus.svelte -->
<script lang="ts">
export let status: 'idle' | 'signing' | 'sending' | 'confirming' | 'success' | 'error';
export let signature: string | null = null;
export let error: string | null = null;
const messages = {
idle: '',
signing: 'Please approve in your wallet...',
sending: 'Sending transaction...',
confirming: 'Confirming on chain...',
success: 'Transaction confirmed!',
error: 'Transaction failed'
};
</script>
{#if status !== 'idle'}
<div class="status" class:success={status === 'success'} class:error={status === 'error'}>
<div class="message">
{#if status === 'signing' || status === 'sending' || status === 'confirming'}
<span class="spinner"></span>
{/if}
{#if status === 'error' && error}
{error}
{:else}
{messages[status]}
{/if}
</div>
{#if status === 'success' && signature}
<a
href={`https://explorer.solana.com/tx/${signature}?cluster=devnet`}
target="_blank"
class="link"
>
View on Explorer →
</a>
{/if}
</div>
{/if}
<style>
.status {
padding: 1rem;
border-radius: 0.5rem;
background: #1a1a2e;
border: 1px solid #333;
}
.status.success {
border-color: #14F195;
background: rgba(20, 241, 149, 0.1);
}
.status.error {
border-color: #f87171;
background: rgba(248, 113, 113, 0.1);
}
.message {
display: flex;
align-items: center;
gap: 0.75rem;
}
.spinner {
width: 1rem;
height: 1rem;
border: 2px solid #333;
border-top-color: #14F195;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.link {
display: block;
margin-top: 0.75rem;
color: #9945FF;
text-decoration: none;
}
.link:hover {
text-decoration: underline;
}
</style>
Transaction Hook Pattern
Combine everything into a reusable pattern:
// src/lib/stores/transaction.ts
import { writable, get } from "svelte/store";
import { connection, wallet } from "./wallet";
import type { Transaction } from "@solana/web3.js";
type TxStatus =
| "idle"
| "signing"
| "sending"
| "confirming"
| "success"
| "error";
interface TransactionState {
status: TxStatus;
signature: string | null;
error: string | null;
}
export function createTransactionStore() {
const { subscribe, set, update } = writable<TransactionState>({
status: "idle",
signature: null,
error: null,
});
return {
subscribe,
reset() {
set({ status: "idle", signature: null, error: null });
},
async send(transaction: Transaction): Promise<boolean> {
const $wallet = get(wallet);
const $connection = get(connection);
if (!$wallet.wallet || !$wallet.publicKey) {
set({
status: "error",
signature: null,
error: "Wallet not connected",
});
return false;
}
try {
// Get blockhash
const { blockhash, lastValidBlockHeight } =
await $connection.getLatestBlockhash("confirmed");
transaction.recentBlockhash = blockhash;
transaction.feePayer = $wallet.publicKey;
// Simulate first
const simulation = await $connection.simulateTransaction(transaction);
if (simulation.value.err) {
throw new Error(
`Simulation failed: ${JSON.stringify(simulation.value.err)}`
);
}
// Sign
set({ status: "signing", signature: null, error: null });
const signedTx = await $wallet.wallet.signTransaction(transaction);
// Send
set({ status: "sending", signature: null, error: null });
const signature = await $connection.sendRawTransaction(
signedTx.serialize(),
{ skipPreflight: true }
);
// Confirm
set({ status: "confirming", signature, error: null });
const confirmation = await $connection.confirmTransaction(
{ signature, blockhash, lastValidBlockHeight },
"confirmed"
);
if (confirmation.value.err) {
throw new Error("Transaction failed on chain");
}
set({ status: "success", signature, error: null });
return true;
} catch (err) {
const error = (err as Error).message;
// User-friendly error messages
let friendlyError = error;
if (error.includes("User rejected")) {
friendlyError = "Transaction cancelled";
} else if (error.includes("insufficient")) {
friendlyError = "Insufficient balance";
} else if (error.includes("block height")) {
friendlyError = "Transaction expired. Please try again.";
}
set({ status: "error", signature: null, error: friendlyError });
return false;
}
},
};
}
export const transaction = createTransactionStore();
Using the Transaction Store
<script lang="ts">
import { transaction } from '$lib/stores/transaction';
import TransactionStatus from '$lib/components/TransactionStatus.svelte';
import { Transaction, SystemProgram, LAMPORTS_PER_SOL } from '@solana/web3.js';
import { wallet } from '$lib/stores/wallet';
async function sendSol() {
transaction.reset();
const tx = new Transaction().add(
SystemProgram.transfer({
fromPubkey: $wallet.publicKey!,
toPubkey: recipientPubkey,
lamports: 0.1 * LAMPORTS_PER_SOL
})
);
const success = await transaction.send(tx);
if (success) {
// Refresh balances, show success, etc.
}
}
</script>
<button on:click={sendSol} disabled={$transaction.status !== 'idle'}>
Send 0.1 SOL
</button>
<TransactionStatus
status={$transaction.status}
signature={$transaction.signature}
error={$transaction.error}
/>
What You Learned
- Common Solana error types and how to handle them
- Simulating transactions before sending
- Retry patterns with exponential backoff
- User-friendly error messages
- Status tracking through the transaction lifecycle
- Building reusable transaction utilities
Next Up
We’ve covered the foundations. Now we bring it all together with x402 - building pay-per-request APIs and clients.
Related Articles
- Understanding requestAnimationFrame
A practical guide to browser animation timing. Learn what requestAnimationFrame actually does, why it beats setInterval, and how to use it properly.
- Lambda Expressions vs Anonymous Functions
When learning a functional programming style you will often come across the term Lambda Expressions or Lambda Functions. In simple terms they are just functions that can be used as data and therefore declared as a value. Let's explore a few examples.
- JavaScript typeof Number
Often you will need to check that you have a number before using it in your JavaScript, here's how.