此内容正在翻译中,完成后将会在此处提供。
Error Handling

Mobile wallet connections fail in ways desktop apps never see. The wallet app might not be installed. The user might switch away before signing. The session might timeout mid-transaction. Bluetooth on some Saga devices can interrupt the connection.
Robust error handling separates production dApps from demos. This lesson gives you patterns for every failure mode in MWA.
MWA Error Types
The MWA SDK throws specific error types. Each requires different handling:
import {
SolanaMobileWalletAdapterError,
SolanaMobileWalletAdapterProtocolError,
} from '@solana-mobile/mobile-wallet-adapter-protocol';SolanaMobileWalletAdapterError: Protocol-level errors (session, transport, association failures)
SolanaMobileWalletAdapterProtocolError: Wallet-level errors (authorization denied, signing rejected)
Protocol Error Codes
MWA defines specific error codes. The SDK wraps protocol errors in JSON-RPC format. Here's the complete reference:
| Code | Constant | Meaning | User Action |
| -32700 | ERROR_JSON_RPC_PARSE | Invalid JSON in request | Bug in your code |
| -32600 | ERROR_JSON_RPC_INVALID_REQUEST | Malformed RPC request | Bug in your code |
| -32601 | ERROR_JSON_RPC_METHOD_NOT_FOUND | Unknown method called | Check method name |
| -32602 | ERROR_JSON_RPC_INVALID_PARAMS | Invalid parameters | Check param types |
| -32603 | ERROR_JSON_RPC_INTERNAL | Wallet internal error | Try again |
| -32000 | ERROR_AUTHORIZATION_FAILED | Auth denied by user | User cancelled. Respect it |
| -32001 | ERROR_INVALID_PAYLOADS | Bad transaction format | Check transaction building |
| -32002 | ERROR_NOT_SIGNED | User declined to sign | User cancelled. Respect it |
| -32003 | ERROR_NOT_SUBMITTED | Wallet couldn't submit | Check network, retry |
| -32004 | ERROR_NOT_CLONED | Clone failed (deprecated) | Rare, retry |
| -32005 | ERROR_TOO_MANY_PAYLOADS | Too many transactions | Batch in smaller groups |
| -32010 | ERROR_ATTEST_ORIGIN_ANDROID | Origin attestation failed | Android config issue |
Structured Error Handler
Build a centralized error handler:
// src/utils/mwaErrorHandler.ts
import {
SolanaMobileWalletAdapterError,
SolanaMobileWalletAdapterProtocolError,
} from '@solana-mobile/mobile-wallet-adapter-protocol';
export interface MWAErrorResult {
userMessage: string;
shouldRetry: boolean;
isUserCancellation: boolean;
originalError: Error;
}
const ERROR_MESSAGES: Record<string, string> = {
ERROR_AUTHORIZATION_FAILED: 'Wallet connection cancelled',
ERROR_NOT_SIGNED: 'Transaction signing declined',
ERROR_NOT_SUBMITTED: 'Could not submit transaction. Check your connection.',
ERROR_TOO_MANY_PAYLOADS: 'Too many transactions. Try sending fewer.',
ERROR_INVALID_PAYLOADS: 'Invalid transaction format',
ERROR_ATTEST_ORIGIN_ANDROID: 'App verification failed. Please reinstall.',
};
export function handleMWAError(error: unknown): MWAErrorResult {
// Not an MWA error
if (!(error instanceof Error)) {
return {
userMessage: 'An unexpected error occurred',
shouldRetry: true,
isUserCancellation: false,
originalError: new Error(String(error)),
};
}
// Protocol-level error (wallet response)
if (error instanceof SolanaMobileWalletAdapterProtocolError) {
const message = ERROR_MESSAGES[error.code] ?? error.message;
const isUserCancellation =
error.code === 'ERROR_AUTHORIZATION_FAILED' ||
error.code === 'ERROR_NOT_SIGNED';
return {
userMessage: message,
shouldRetry: !isUserCancellation,
isUserCancellation,
originalError: error,
};
}
// SDK-level error (association, session)
if (error instanceof SolanaMobileWalletAdapterError) {
// Common: no wallet installed
if (error.message.includes('Found no installed wallet')) {
return {
userMessage: 'No Solana wallet found. Please install a wallet app.',
shouldRetry: false,
isUserCancellation: false,
originalError: error,
};
}
// Session timeout
if (error.message.includes('timeout') || error.message.includes('Timeout')) {
return {
userMessage: 'Wallet connection timed out. Please try again.',
shouldRetry: true,
isUserCancellation: false,
originalError: error,
};
}
// Generic SDK error
return {
userMessage: 'Could not connect to wallet. Please try again.',
shouldRetry: true,
isUserCancellation: false,
originalError: error,
};
}
// Standard Error (network issues, etc.)
if (error.message.includes('Network') || error.message.includes('fetch')) {
return {
userMessage: 'Network error. Check your connection.',
shouldRetry: true,
isUserCancellation: false,
originalError: error,
};
}
return {
userMessage: error.message || 'An error occurred',
shouldRetry: true,
isUserCancellation: false,
originalError: error,
};
}Using the Error Handler
Wrap your transact calls:
import { Alert } from 'react-native';
import { transact } from '@solana-mobile/mobile-wallet-adapter-protocol-web3js';
import { handleMWAError } from '../utils/mwaErrorHandler';
async function connectWallet() {
try {
await transact(async (wallet) => {
const result = await wallet.authorize({
identity: APP_IDENTITY,
chain: 'solana:devnet',
});
// Handle success
});
} catch (error) {
const { userMessage, isUserCancellation, shouldRetry } = handleMWAError(error);
if (isUserCancellation) {
// User intentionally cancelled—don't show error
console.log('User cancelled');
return;
}
if (shouldRetry) {
Alert.alert('Error', userMessage, [
{ text: 'Cancel', style: 'cancel' },
{ text: 'Retry', onPress: () => connectWallet() },
]);
} else {
Alert.alert('Error', userMessage);
}
}
}Retry Patterns
Implement exponential backoff for transient failures:
// src/utils/retry.ts
export interface RetryOptions {
maxAttempts: number;
baseDelayMs: number;
maxDelayMs: number;
}
const DEFAULT_OPTIONS: RetryOptions = {
maxAttempts: 3,
baseDelayMs: 1000,
maxDelayMs: 10000,
};
export async function withRetry<T>(
operation: () => Promise<T>,
shouldRetry: (error: unknown) => boolean,
options: Partial<RetryOptions> = {}
): Promise<T> {
const { maxAttempts, baseDelayMs, maxDelayMs } = {
...DEFAULT_OPTIONS,
...options,
};
let lastError: unknown;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await operation();
} catch (error) {
lastError = error;
if (!shouldRetry(error) || attempt === maxAttempts) {
throw error;
}
// Exponential backoff with jitter
const delay = Math.min(
baseDelayMs * Math.pow(2, attempt - 1) + Math.random() * 1000,
maxDelayMs
);
console.log(`Attempt ${attempt} failed, retrying in ${delay}ms...`);
await new Promise((resolve) => setTimeout(resolve, delay));
}
}
throw lastError;
}Use with MWA:
import { handleMWAError } from '../utils/mwaErrorHandler';
import { withRetry } from '../utils/retry';
async function sendTransactionWithRetry(transaction: VersionedTransaction) {
return await withRetry(
async () => {
return await transact(async (wallet) => {
await authorizeSession(wallet);
const [signature] = await wallet.signAndSendTransactions({
transactions: [transaction],
});
return signature;
});
},
(error) => {
const result = handleMWAError(error);
return result.shouldRetry && !result.isUserCancellation;
},
{ maxAttempts: 3 }
);
}Session Lifecycle Errors
The MWA session can end unexpectedly:
User switches away: The session stays alive briefly, but the wallet might close it.
Wallet backgrounded too long: Android may kill the wallet process.
Auth token expired: Tokens don't last forever.
Handle session errors by re-authorizing:
async function robustTransact<T>(
callback: (wallet: Web3MobileWallet) => Promise<T>
): Promise<T> {
return await transact(async (wallet) => {
try {
return await callback(wallet);
} catch (error) {
// If it's an auth error, the token might be stale
if (
error instanceof SolanaMobileWalletAdapterProtocolError &&
error.code === 'ERROR_AUTHORIZATION_FAILED'
) {
// Clear cached token
await AsyncStorage.removeItem('mwa_auth_token');
// Re-authorize without cached token
await wallet.authorize({
identity: APP_IDENTITY,
chain: 'solana:devnet',
});
// Retry the operation
return await callback(wallet);
}
throw error;
}
});
}No Wallet Installed
Handle the case where no wallet is installed:
import { Linking, Platform } from 'react-native';
const WALLET_STORE_URLS = {
phantom: {
ios: 'https://apps.apple.com/app/phantom-solana-wallet/id1598432977',
android: 'https://play.google.com/store/apps/details?id=app.phantom',
},
solflare: {
ios: 'https://apps.apple.com/app/solflare/id1580902717',
android: 'https://play.google.com/store/apps/details?id=com.solflare.mobile',
},
};
function NoWalletPrompt() {
const openStore = async (wallet: 'phantom' | 'solflare') => {
const url = WALLET_STORE_URLS[wallet][Platform.OS === 'ios' ? 'ios' : 'android'];
await Linking.openURL(url);
};
return (
<View style={styles.container}>
<Text style={styles.title}>No Wallet Found</Text>
<Text style={styles.subtitle}>
Install a Solana wallet to continue
</Text>
<Button
title="Get Phantom"
onPress={() => openStore('phantom')}
/>
<Button
title="Get Solflare"
onPress={() => openStore('solflare')}
/>
</View>
);
}Integrate with your connect flow:
function ConnectButton() {
const [showNoWallet, setShowNoWallet] = useState(false);
const handleConnect = async () => {
try {
await transact(async (wallet) => {
await wallet.authorize({ /* ... */ });
});
} catch (error) {
const result = handleMWAError(error);
if (result.userMessage.includes('No Solana wallet')) {
setShowNoWallet(true);
return;
}
if (!result.isUserCancellation) {
Alert.alert('Error', result.userMessage);
}
}
};
if (showNoWallet) {
return <NoWalletPrompt />;
}
return <Button title="Connect Wallet" onPress={handleConnect} />;
}Transaction Simulation Errors
Before sending to a wallet, simulate transactions to catch errors early:
import {
Connection,
VersionedTransaction,
SendTransactionError,
} from '@solana/web3.js';
interface SimulationResult {
success: boolean;
error?: string;
logs?: string[];
}
async function simulateTransaction(
connection: Connection,
transaction: VersionedTransaction
): Promise<SimulationResult> {
try {
const result = await connection.simulateTransaction(transaction, {
commitment: 'confirmed',
});
if (result.value.err) {
return {
success: false,
error: formatSimulationError(result.value.err),
logs: result.value.logs ?? undefined,
};
}
return { success: true, logs: result.value.logs ?? undefined };
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Simulation failed',
};
}
}
function formatSimulationError(err: any): string {
if (typeof err === 'string') return err;
if (err.InstructionError) {
const [index, reason] = err.InstructionError;
if (typeof reason === 'object' && reason.Custom !== undefined) {
return `Instruction ${index} failed with custom error ${reason.Custom}`;
}
return `Instruction ${index} failed: ${JSON.stringify(reason)}`;
}
return JSON.stringify(err);
}Use it before wallet signing:
async function sendWithSimulation(
transaction: VersionedTransaction,
connection: Connection
): Promise<string> {
// Simulate first
const simulation = await simulateTransaction(connection, transaction);
if (!simulation.success) {
throw new Error(`Transaction would fail: ${simulation.error}`);
}
// Only open wallet if simulation passed
return await transact(async (wallet) => {
await authorizeSession(wallet);
const [signature] = await wallet.signAndSendTransactions({
transactions: [transaction],
});
return signature;
});
}User Feedback Component
Create a component for transaction status:
// components/TransactionStatus.tsx
import React from 'react';
import { View, Text, ActivityIndicator, StyleSheet } from 'react-native';
type Status = 'idle' | 'simulating' | 'signing' | 'confirming' | 'success' | 'error';
interface Props {
status: Status;
signature?: string;
error?: string;
}
const STATUS_MESSAGES: Record<Status, string> = {
idle: '',
simulating: 'Simulating transaction...',
signing: 'Approve in your wallet...',
confirming: 'Confirming on chain...',
success: 'Transaction confirmed!',
error: 'Transaction failed',
};
export function TransactionStatus({ status, signature, error }: Props) {
if (status === 'idle') return null;
return (
<View style={styles.container}>
{(status === 'simulating' || status === 'signing' || status === 'confirming') && (
<ActivityIndicator size="small" />
)}
<Text style={[styles.text, status === 'error' && styles.errorText]}>
{STATUS_MESSAGES[status]}
</Text>
{status === 'error' && error && (
<Text style={styles.errorDetail}>{error}</Text>
)}
{status === 'success' && signature && (
<Text style={styles.signature}>
{signature.slice(0, 20)}...
</Text>
)}
</View>
);
}Logging for Debug
In development, log all MWA interactions:
// src/utils/mwaLogger.ts
const DEBUG = __DEV__;
export function logMWARequest(method: string, params: unknown): void {
if (!DEBUG) return;
console.log(`[MWA] → ${method}`, JSON.stringify(params, null, 2));
}
export function logMWAResponse(method: string, result: unknown): void {
if (!DEBUG) return;
console.log(`[MWA] ← ${method}`, JSON.stringify(result, null, 2));
}
export function logMWAError(method: string, error: unknown): void {
console.error(`[MWA] ✕ ${method}`, error);
}Wrap your transact calls:
async function trackedTransact<T>(
callback: (wallet: Web3MobileWallet) => Promise<T>
): Promise<T> {
const startTime = Date.now();
try {
const result = await transact(callback);
console.log(`[MWA] Session completed in ${Date.now() - startTime}ms`);
return result;
} catch (error) {
console.error(`[MWA] Session failed after ${Date.now() - startTime}ms`, error);
throw error;
}
}Good error handling makes your dApp feel professional. In the next lesson, we'll cover testing your implementation on actual devices.