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 messagestatus: HTTP status code (if applicable)code: Error code for programmatic handlingresponse: 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:
- Prompt user to re-authenticate
- Generate new device key
- 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:
- Clear device key cache
- Regenerate device key
- Re-register with API
- 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:
- Generate and add new device identity
- Restore from backup
- 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:
- Use
getBestDeviceIdentity()to auto-select - Prompt user to select device
- 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
| Error | Cause | Recovery |
|---|---|---|
DeviceKeyNotFoundError | Device key not in storage | Re-authenticate or restore key |
DeviceKeyMismatchError | Stored key doesn't match API | Rotate key or re-register |
NoDeviceIdentitiesError | No device keys for signer | Add device identity |
MultipleDeviceIdentitiesError | Multiple devices registered | Specify which device to use |
InsufficientFundsError | Not enough balance | Add funds to wallet |
TransactionFailedError | Transaction rejected by network | Check parameters and retry |
BlockhashExpiredError | Transaction took too long to sign | Retry 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 }
);