Error Handling

Handle errors gracefully with comprehensive error types and recovery strategies

Error Handling

The Cilantro Smart SDK provides specific error types for better error handling and debugging. This guide covers all error scenarios and best practices.

Response and error utilities

Use extractResponseData to unwrap controller-style { data: T } responses and extractErrorMessage to get a user-facing message from thrown errors or error-shaped responses. See Response and errors for the full helper implementations.

Example with try/catch:

import { create } from 'cilantro-sdk/wallet';

try {
  const result = await create({ walletName: 'My Wallet' });
  const wallet = extractResponseData(result);
} catch (e) {
  console.error(extractErrorMessage(e));
}

Use these consistently across your app; for "device key not found" (email/phone signers), show a clear message like "Re-create the signer on this device."

Error Classes

The SDK exports several error classes for different error scenarios:

import { 
  SDKError,
  DeviceKeyMismatchError,
  DeviceKeyNotFoundError,
  NoDeviceIdentitiesError,
  MultipleDeviceIdentitiesError
} from 'cilantro-sdk';

Common Error Types

SDKError

The base error class for all SDK-related errors.

import { sendSOL } from 'cilantro-sdk/wallet';
import { SDKError } from 'cilantro-sdk';

try {
  const result = await sendSOL('wallet-id', {
    recipientAddress: 'DYw8jCTfwHNRJhhmFcbXvVDTqWMEVFBX6ZKUmG5CNSKK',
    amountLamports: 1000000000
  });
} catch (error) {
  if (error instanceof SDKError) {
    console.error('SDK Error:', error.message);
    console.error('Status Code:', error.status);
    console.error('Error Code:', error.code);
    console.error('Response:', error.response);
  }
}

Properties:

  • message: Human-readable error message
  • status: HTTP status code (if applicable)
  • code: Error code for programmatic handling
  • response: Full API response (if applicable)

DeviceKeyNotFoundError

Thrown when a device key cannot be found in storage.

import { getEmailSignerKeypair } from 'cilantro-sdk/helpers';
import { DeviceKeyNotFoundError } from 'cilantro-sdk';

try {
  const keypair = await getEmailSignerKeypair(walletId, signerId, { 
    deviceKeyManager: storage 
  });
} catch (error) {
  if (error instanceof DeviceKeyNotFoundError) {
    console.error('Device key not found:', error.devicePublicKey);
    console.error('Please log in on this device again');
    
    // Recovery: Prompt user to re-authenticate
    await promptUserToReauthenticate();
  }
}

Common Causes:

  • User cleared browser data / localStorage
  • Different browser or device
  • Storage adapter not configured
  • Device key was manually deleted

Recovery Strategies:

  1. Prompt user to re-authenticate
  2. Generate new device key
  3. Restore from backup if available

DeviceKeyMismatchError

Thrown when a device key in storage doesn't match the API.

import { DeviceKeyMismatchError } from 'cilantro-sdk';

try {
  const keypair = await getEmailSignerKeypair(walletId, signerId, { 
    deviceKeyManager: storage 
  });
} catch (error) {
  if (error instanceof DeviceKeyMismatchError) {
    console.error('Device key mismatch');
    console.error('Expected:', error.expectedDeviceKey);
    console.error('Actual:', error.actualDeviceKey);
    
    // Recovery: Clear cache and regenerate
    clearDeviceKeyCache();
    await generateNewDeviceKey();
  }
}

Common Causes:

  • Storage corruption
  • Manual key modification
  • Sync issues across devices
  • Security breach attempt

Recovery Strategies:

  1. Clear device key cache
  2. Regenerate device key
  3. Re-register with API
  4. Verify security (check for unauthorized access)

NoDeviceIdentitiesError

Thrown when a signer has no device identities registered.

import { NoDeviceIdentitiesError } from 'cilantro-sdk';

try {
  const keypair = await getEmailSignerKeypair(walletId, signerId, { 
    deviceKeyManager: storage 
  });
} catch (error) {
  if (error instanceof NoDeviceIdentitiesError) {
    console.error('No device keys registered for this signer');
    
    // Recovery: Add device identity
    const deviceKey = await generateDeviceKeyPair();
    await addDeviceIdentityToSigner(walletId, signerId, {
      devicePublicKey: deviceKey.publicKey,
      deviceId: deviceKey.keyId
    });
  }
}

Common Causes:

  • New signer without device setup
  • All device identities were removed
  • API sync issue

Recovery Strategies:

  1. Generate and add new device identity
  2. Restore from backup
  3. Use alternative signer

MultipleDeviceIdentitiesError

Thrown when multiple device identities are found and resolution is ambiguous.

import { MultipleDeviceIdentitiesError } from 'cilantro-sdk';
import { getBestDeviceIdentity } from 'cilantro-sdk/helpers';

try {
  const keypair = await getEmailSignerKeypair(walletId, signerId, { 
    deviceKeyManager: storage 
  });
} catch (error) {
  if (error instanceof MultipleDeviceIdentitiesError) {
    console.error('Multiple device identities found:', error.deviceIdentities);
    
    // Recovery: Use best device identity (most recently used)
    const bestDevice = await getBestDeviceIdentity(walletId, signerId, {
      deviceKeyManager: storage
    });
    
    // Retry with specific device
    const keypair = await getEmailSignerKeypair(walletId, signerId, { 
      deviceKeyManager: storage,
      deviceId: bestDevice.deviceId
    });
  }
}

Common Causes:

  • User has multiple devices registered
  • No specific device ID provided
  • Device selection needed

Recovery Strategies:

  1. Use getBestDeviceIdentity() to auto-select
  2. Prompt user to select device
  3. Use most recently used device

Transaction Errors

Insufficient Funds

try {
  await sendSOL(walletId, {
    recipientAddress: 'address...',
    amountLamports: 10000000000 // 10 SOL
  });
} catch (error) {
  if (error.message.includes('insufficient funds')) {
    console.error('Wallet has insufficient balance');
    
    // Show user-friendly message
    showError(`Insufficient balance. You need at least ${requiredAmount} SOL.`);
    
    // Offer to add funds
    promptUserToAddFunds();
  }
}

Blockhash Expired

try {
  await submitTransaction(walletId, { signedTransaction });
} catch (error) {
  if (error.message.includes('blockhash') || error.message.includes('expired')) {
    console.error('Transaction blockhash expired');
    
    // Recovery: Retry with new blockhash
    const prepared = await prepareTransaction(walletId, params);
    // Sign and submit again...
  }
}

Transaction Failed

try {
  const result = await sendSOL(walletId, params);
  await waitForConfirmation(result.data.signature);
} catch (error) {
  if (error.message.includes('failed')) {
    console.error('Transaction failed on-chain:', error.message);
    
    // Check specific failure reason
    const status = await getTransactionStatus(signature);
    console.error('Failure reason:', status.data.error);
    
    // Handle based on failure type
    if (status.data.error.includes('custom program error')) {
      // Program-specific error
    }
  }
}

Complete Error Handling Example

import { 
  getEmailSignerKeypair,
  createLocalStorageAdapter,
  clearDeviceKeyCache,
  getBestDeviceIdentity 
} from 'cilantro-sdk/helpers';
import { 
  SDKError,
  DeviceKeyNotFoundError,
  DeviceKeyMismatchError,
  NoDeviceIdentitiesError,
  MultipleDeviceIdentitiesError
} from 'cilantro-sdk';

async function getKeypairWithErrorHandling(
  walletId: string, 
  signerId: string
) {
  const storage = createLocalStorageAdapter();
  
  try {
    const keypair = await getEmailSignerKeypair(walletId, signerId, { 
      deviceKeyManager: storage 
    });
    return keypair;
    
  } catch (error) {
    // Handle device key errors
    if (error instanceof DeviceKeyNotFoundError) {
      console.error('Device key not found:', error.devicePublicKey);
      throw new Error('Device key not found. Please log in again.');
      
    } else if (error instanceof DeviceKeyMismatchError) {
      console.error('Device key mismatch');
      console.error('Expected:', error.expectedDeviceKey);
      console.error('Actual:', error.actualDeviceKey);
      
      // Clear cache and prompt re-authentication
      clearDeviceKeyCache();
      throw new Error('Device key mismatch. Please re-authenticate.');
      
    } else if (error instanceof NoDeviceIdentitiesError) {
      console.error('No device identities found for this signer');
      throw new Error('No device found. Please set up device authentication.');
      
    } else if (error instanceof MultipleDeviceIdentitiesError) {
      console.error('Multiple devices found:', error.deviceIdentities);
      
      // Automatically use best device (most recently used)
      const bestDevice = await getBestDeviceIdentity(walletId, signerId, {
        deviceKeyManager: storage
      });
      
      console.log('Using device:', bestDevice.deviceId);
      
      // Retry with specific device
      return await getEmailSignerKeypair(walletId, signerId, { 
        deviceKeyManager: storage,
        deviceId: bestDevice.deviceId
      });
      
    } else if (error instanceof SDKError) {
      console.error('SDK Error:', error.message);
      console.error('Status:', error.status);
      console.error('Code:', error.code);
      throw error;
      
    } else {
      console.error('Unexpected error:', error);
      throw error;
    }
  }
}

Error Summary Table

ErrorCauseRecovery
DeviceKeyNotFoundErrorDevice key not in storageRe-authenticate or restore key
DeviceKeyMismatchErrorStored key doesn't match APIRotate key or re-register
NoDeviceIdentitiesErrorNo device keys for signerAdd device identity
MultipleDeviceIdentitiesErrorMultiple devices registeredSpecify which device to use
InsufficientFundsErrorNot enough balanceAdd funds to wallet
TransactionFailedErrorTransaction rejected by networkCheck parameters and retry
BlockhashExpiredErrorTransaction took too long to signRetry with new blockhash

Retry Logic with Exponential Backoff

async function withRetry<T>(
  operation: () => Promise<T>,
  maxRetries: number = 3,
  baseDelay: number = 1000
): Promise<T> {
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      return await operation();
    } catch (error) {
      if (attempt === maxRetries) {
        throw new Error(`Failed after ${maxRetries} attempts: ${error.message}`);
      }
      
      // Check if error is retryable
      const isRetryable = 
        error.message.includes('timeout') ||
        error.message.includes('network') ||
        error.message.includes('blockhash');
      
      if (!isRetryable) {
        throw error; // Don't retry non-retryable errors
      }
      
      // Exponential backoff
      const delay = baseDelay * Math.pow(2, attempt - 1);
      console.log(`Attempt ${attempt} failed. Retrying in ${delay}ms...`);
      await new Promise(resolve => setTimeout(resolve, delay));
    }
  }
  
  throw new Error('Unreachable');
}

// Usage
const result = await withRetry(
  () => sendSOL(walletId, params),
  3, // max retries
  2000 // base delay (2s)
);

User-Friendly Error Messages

function getUserFriendlyErrorMessage(error: Error): string {
  if (error instanceof DeviceKeyNotFoundError) {
    return 'Your device key was not found. Please log in again on this device.';
  }
  
  if (error instanceof DeviceKeyMismatchError) {
    return 'Device key verification failed. Please re-authenticate for security.';
  }
  
  if (error instanceof NoDeviceIdentitiesError) {
    return 'No devices are registered for this wallet. Please set up device authentication.';
  }
  
  if (error instanceof MultipleDeviceIdentitiesError) {
    return 'Multiple devices found. We\'ll use your most recent one.';
  }
  
  if (error.message.includes('insufficient funds')) {
    return 'Insufficient balance. Please add funds to your wallet.';
  }
  
  if (error.message.includes('blockhash expired')) {
    return 'Transaction expired. Please try again.';
  }
  
  if (error.message.includes('failed')) {
    return 'Transaction failed. Please check the transaction details and try again.';
  }
  
  if (error instanceof SDKError) {
    return `An error occurred: ${error.message}`;
  }
  
  return 'An unexpected error occurred. Please try again or contact support.';
}

// Usage in UI
try {
  await sendSOL(walletId, params);
} catch (error) {
  const userMessage = getUserFriendlyErrorMessage(error);
  showErrorToUser(userMessage);
  
  // Log technical details for debugging
  console.error('Technical error:', error);
}

Validation Before Operations

Prevent errors by validating inputs before API calls:

function validateSolanaAddress(address: string): boolean {
  // Basic Solana address validation
  return /^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(address);
}

function validateAmount(amount: number): boolean {
  return amount > 0 && Number.isInteger(amount);
}

async function sendSOLWithValidation(
  walletId: string,
  recipientAddress: string,
  amountLamports: number
) {
  // Validate inputs
  if (!validateSolanaAddress(recipientAddress)) {
    throw new Error('Invalid recipient address format');
  }
  
  if (!validateAmount(amountLamports)) {
    throw new Error('Invalid amount. Must be a positive integer.');
  }
  
  // Check wallet balance before sending
  const wallet = await findOne(walletId);
  if (wallet.data.balance < amountLamports) {
    throw new Error(`Insufficient balance. Have: ${wallet.data.balance}, Need: ${amountLamports}`);
  }
  
  // Proceed with transaction
  return await sendSOL(walletId, {
    recipientAddress,
    amountLamports
  });
}

Error Monitoring and Logging

class ErrorLogger {
  static logError(error: Error, context: Record<string, any>) {
    const errorInfo = {
      timestamp: new Date().toISOString(),
      message: error.message,
      type: error.constructor.name,
      stack: error.stack,
      context,
      // Don't log sensitive data
      user: context.userId ? `user-${context.userId.substring(0, 8)}` : undefined
    };
    
    // Log to console in development
    if (process.env.NODE_ENV === 'development') {
      console.error('Error logged:', errorInfo);
    }
    
    // Send to error tracking service (e.g., Sentry)
    if (process.env.NODE_ENV === 'production') {
      // sendToErrorTracking(errorInfo);
    }
  }
}

// Usage
try {
  await sendSOL(walletId, params);
} catch (error) {
  ErrorLogger.logError(error, {
    operation: 'sendSOL',
    walletId,
    userId: currentUser.id
    // Don't log amount, addresses, or other sensitive data
  });
  
  throw error;
}

Best Practices

Production Error Handling Template

import { 
  SDKError,
  DeviceKeyNotFoundError,
  DeviceKeyMismatchError,
  NoDeviceIdentitiesError,
  MultipleDeviceIdentitiesError
} from 'cilantro-sdk';

async function handleOperation<T>(
  operation: () => Promise<T>,
  context: Record<string, any>
): Promise<T> {
  try {
    return await operation();
  } catch (error) {
    // Log error securely
    ErrorLogger.logError(error, context);
    
    // Handle specific error types
    if (error instanceof DeviceKeyNotFoundError) {
      throw new Error('Device authentication required. Please log in again.');
    }
    
    if (error instanceof DeviceKeyMismatchError) {
      clearDeviceKeyCache();
      throw new Error('Security verification failed. Please re-authenticate.');
    }
    
    if (error instanceof NoDeviceIdentitiesError) {
      throw new Error('Device setup required. Please complete device registration.');
    }
    
    if (error instanceof MultipleDeviceIdentitiesError) {
      // Auto-select best device and retry
      // Implementation depends on specific use case
    }
    
    if (error instanceof SDKError) {
      throw new Error(`Operation failed: ${error.message}`);
    }
    
    // Unexpected error
    throw new Error('An unexpected error occurred. Please try again or contact support.');
  }
}

// Usage
const result = await handleOperation(
  () => sendSOL(walletId, params),
  { operation: 'sendSOL', walletId }
);

Next Steps

Error Handling | Cilantro Smart Wallet Docs | Cilantro Smart Wallet