Error Handling
Comprehensive guide to handling errors in the Aegis SDK.
Error Classes
The SDK provides typed error classes for better error handling:
import {
AegisError, // Base error class
DailyLimitExceededError, // Daily limit exceeded
NotWhitelistedError, // Address not whitelisted
VaultPausedError, // Vault is paused
InsufficientBalanceError, // Not enough SOL
UnauthorizedSignerError, // Wrong signer
MissingWalletError, // No wallet set
NetworkError, // Network/RPC error
TransactionTimeoutError, // Transaction timed out
} from '@aegis-vaults/sdk';Policy Errors
These errors indicate a transaction was blocked by vault policy.
NotWhitelistedError
Thrown when destination address is not in the whitelist.
class NotWhitelistedError extends AegisError {
destination: string; // The blocked address
overrideRequested: boolean; // If override was requested
blinkUrl?: string; // Approval URL (if auto-override enabled)
}Example:
try {
await client.executeAgent({
vault: vaultAddress,
destination: newAddress, // Not whitelisted!
amount: 10_000_000,
vaultNonce,
});
} catch (error) {
if (error instanceof NotWhitelistedError) {
console.log('❌ Address not whitelisted:', error.destination);
if (error.overrideRequested) {
console.log('📱 Approval URL:', error.blinkUrl);
// Vault owner can approve via Blink
} else {
// Request owner to whitelist the address
await requestWhitelist(error.destination);
}
}
}DailyLimitExceededError
Thrown when transaction would exceed the daily spending limit.
class DailyLimitExceededError extends AegisError {
amount: number; // Attempted amount
dailyLimit: number; // Vault's daily limit
spentToday: number; // Already spent today
remaining: number; // Remaining limit
overrideRequested: boolean;
blinkUrl?: string;
}Example:
try {
await client.executeAgent({
vault: vaultAddress,
destination: recipientAddress,
amount: 2_000_000_000, // 2 SOL - exceeds limit!
vaultNonce,
});
} catch (error) {
if (error instanceof DailyLimitExceededError) {
console.log('❌ Daily limit exceeded');
console.log('Attempted:', error.amount / 1e9, 'SOL');
console.log('Daily Limit:', error.dailyLimit / 1e9, 'SOL');
console.log('Spent Today:', error.spentToday / 1e9, 'SOL');
console.log('Remaining:', error.remaining / 1e9, 'SOL');
if (error.overrideRequested) {
console.log('📱 Override requested:', error.blinkUrl);
// Owner can approve this specific transaction
}
}
}VaultPausedError
Thrown when vault is paused (emergency stop).
class VaultPausedError extends AegisError {
vault: string; // Vault address
}Example:
try {
await client.executeAgent({...});
} catch (error) {
if (error instanceof VaultPausedError) {
console.log('🛑 Vault is paused');
console.log('Vault:', error.vault);
// Notify admin to resume vault
await notifyAdmin({
type: 'vault_paused',
vault: error.vault,
message: 'Cannot execute transactions while paused',
});
// Wait or fail gracefully
throw new Error('Vault paused - contact vault owner');
}
}Balance Errors
InsufficientBalanceError
Thrown when vault doesn't have enough SOL.
class InsufficientBalanceError extends AegisError {
required: number; // Required amount
available: number; // Current balance
shortfall: number; // Missing amount
}Example:
try {
await client.executeAgent({
amount: 5_000_000_000, // 5 SOL
// ...
});
} catch (error) {
if (error instanceof InsufficientBalanceError) {
console.log('❌ Insufficient balance');
console.log('Required:', error.required / 1e9, 'SOL');
console.log('Available:', error.available / 1e9, 'SOL');
console.log('Shortfall:', error.shortfall / 1e9, 'SOL');
// Notify owner to fund vault
await notifyOwner({
type: 'low_balance',
required: error.required,
available: error.available,
depositAddress: client.getVaultDepositAddress(vaultAddress),
});
}
}Authorization Errors
UnauthorizedSignerError
Thrown when signer doesn't match vault's authorized agent.
class UnauthorizedSignerError extends AegisError {
expectedSigner: string; // Vault's agent_signer
actualSigner: string; // Your keypair
}Example:
try {
await client.executeAgent({...});
} catch (error) {
if (error instanceof UnauthorizedSignerError) {
console.log('❌ Unauthorized signer');
console.log('Expected:', error.expectedSigner);
console.log('Actual:', error.actualSigner);
// This is a critical error - check your configuration
console.error('CRITICAL: Using wrong agent keypair!');
process.exit(1);
}
}MissingWalletError
Thrown when no wallet is set on the client.
class MissingWalletError extends AegisError {}Example:
const client = new AegisClient({...});
// Forgot to call client.setWallet()
try {
await client.executeAgent({...});
} catch (error) {
if (error instanceof MissingWalletError) {
console.log('❌ No wallet set');
// Fix: Set wallet
client.setWallet(agentKeypair);
}
}Network Errors
NetworkError
Thrown when RPC or network issues occur.
class NetworkError extends AegisError {
originalError: Error; // Original error from Solana
}Example:
try {
await client.executeAgent({...});
} catch (error) {
if (error instanceof NetworkError) {
console.log('❌ Network error:', error.message);
console.log('Original:', error.originalError);
// Retry with exponential backoff
await retryWithBackoff(() => client.executeAgent({...}));
}
}TransactionTimeoutError
Thrown when transaction doesn't confirm within timeout.
class TransactionTimeoutError extends AegisError {
signature?: string; // May have signature even if timed out
}Example:
try {
await client.executeAgent({...});
} catch (error) {
if (error instanceof TransactionTimeoutError) {
console.log('⏱️ Transaction timed out');
if (error.signature) {
console.log('Signature:', error.signature);
console.log('Check explorer - may have succeeded');
// Wait and check if transaction actually went through
await new Promise(resolve => setTimeout(resolve, 10000));
const vault = await client.getVault(vaultAddress);
// Check if spent_today increased
}
}
}Error Handling Patterns
Comprehensive Try-Catch
Handle all error types:
async function executeTransaction(
client: AegisClient,
options: ExecuteAgentOptions
) {
try {
const signature = await client.executeAgent(options);
return { success: true, signature };
} catch (error) {
// Policy errors
if (error instanceof NotWhitelistedError) {
return {
success: false,
reason: 'not_whitelisted',
destination: error.destination,
blinkUrl: error.blinkUrl,
};
}
if (error instanceof DailyLimitExceededError) {
return {
success: false,
reason: 'limit_exceeded',
remaining: error.remaining,
blinkUrl: error.blinkUrl,
};
}
if (error instanceof VaultPausedError) {
return {
success: false,
reason: 'vault_paused',
vault: error.vault,
};
}
// Balance errors
if (error instanceof InsufficientBalanceError) {
return {
success: false,
reason: 'insufficient_balance',
shortfall: error.shortfall,
};
}
// Auth errors
if (error instanceof UnauthorizedSignerError) {
console.error('CRITICAL: Wrong signer');
throw error; // Critical error - rethrow
}
// Network errors
if (error instanceof NetworkError) {
console.log('Network error, retrying...');
// Retry logic
return await retryTransaction(client, options);
}
if (error instanceof TransactionTimeoutError) {
console.log('Timeout - checking transaction status...');
// Check if actually succeeded
return await checkTransactionStatus(error.signature);
}
// Unknown error
console.error('Unknown error:', error);
throw error;
}
}Retry Logic
Retry network errors with exponential backoff:
async function retryWithBackoff<T>(
fn: () => Promise<T>,
maxRetries: number = 3
): Promise<T> {
let lastError: any;
for (let i = 0; i < maxRetries; i++) {
try {
return await fn();
} catch (error) {
lastError = error;
// Don't retry policy errors
if (
error instanceof NotWhitelistedError ||
error instanceof DailyLimitExceededError ||
error instanceof VaultPausedError ||
error instanceof UnauthorizedSignerError
) {
throw error;
}
// Exponential backoff
const delay = Math.pow(2, i) * 1000;
console.log(`Retry ${i + 1}/${maxRetries} after ${delay}ms`);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
throw lastError;
}
// Usage
const result = await retryWithBackoff(() =>
client.executeAgent({...})
);Circuit Breaker
Prevent cascading failures:
class CircuitBreaker {
private failures = 0;
private lastFailure = 0;
private readonly threshold = 5;
private readonly resetTimeout = 60000; // 1 minute
async execute<T>(fn: () => Promise<T>): Promise<T> {
// Check if circuit is open
if (this.failures >= this.threshold) {
if (Date.now() - this.lastFailure < this.resetTimeout) {
throw new Error('Circuit breaker is open');
}
// Reset after timeout
this.failures = 0;
}
try {
const result = await fn();
this.failures = 0; // Reset on success
return result;
} catch (error) {
this.failures++;
this.lastFailure = Date.now();
throw error;
}
}
}
const breaker = new CircuitBreaker();
// Usage
const result = await breaker.execute(() =>
client.executeAgent({...})
);Graceful Degradation
Provide fallback behavior:
async function executeWithFallback(
client: AegisClient,
options: ExecuteAgentOptions
) {
try {
// Try primary method
return await client.executeAgent(options);
} catch (error) {
if (error instanceof DailyLimitExceededError) {
// Fallback: Queue for next day
await queueForNextDay(options);
return { status: 'queued_for_tomorrow' };
}
if (error instanceof InsufficientBalanceError) {
// Fallback: Request funding
await requestFunding(error.shortfall);
return { status: 'funding_requested' };
}
// No fallback available
throw error;
}
}Error Logging
Structured Error Logging
import pino from 'pino';
const logger = pino();
async function executeWithLogging(
client: AegisClient,
options: ExecuteAgentOptions
) {
try {
const signature = await client.executeAgent(options);
logger.info({
event: 'transaction_success',
signature,
vault: options.vault,
destination: options.destination,
amount: options.amount,
});
return signature;
} catch (error: any) {
logger.error({
event: 'transaction_failed',
error: error.message,
errorType: error.constructor.name,
vault: options.vault,
destination: options.destination,
amount: options.amount,
stack: error.stack,
});
throw error;
}
}Error Metrics
Track error rates:
import { Counter } from 'prom-client';
const errorCounter = new Counter({
name: 'aegis_errors_total',
help: 'Total errors by type',
labelNames: ['error_type', 'vault'],
});
async function executeWithMetrics(
client: AegisClient,
options: ExecuteAgentOptions
) {
try {
return await client.executeAgent(options);
} catch (error: any) {
errorCounter.inc({
error_type: error.constructor.name,
vault: options.vault,
});
throw error;
}
}Testing Error Cases
Simulate Errors
describe('Error Handling', () => {
it('handles NotWhitelistedError', async () => {
const nonWhitelistedAddress = Keypair.generate().publicKey;
await expect(
client.executeAgent({
vault: vaultAddress,
destination: nonWhitelistedAddress.toBase58(),
amount: 10_000_000,
vaultNonce,
})
).rejects.toThrow(NotWhitelistedError);
});
it('handles DailyLimitExceededError', async () => {
const vault = await client.getVault(vaultAddress);
const tooMuch = vault.dailyLimit.toNumber() + 1;
await expect(
client.executeAgent({
vault: vaultAddress,
destination: whitelistedAddress,
amount: tooMuch,
vaultNonce,
})
).rejects.toThrow(DailyLimitExceededError);
});
it('handles VaultPausedError', async () => {
// Pause vault
client.setWallet(ownerKeypair);
await client.pauseVault(vaultAddress, vaultNonce);
// Try to execute as agent
client.setWallet(agentKeypair);
await expect(
client.executeAgent({...})
).rejects.toThrow(VaultPausedError);
});
});Best Practices
- Always catch errors - Don't let errors crash your application
- Use typed errors - Check
instanceoffor specific handling - Log errors - Use structured logging for debugging
- Retry network errors - Implement exponential backoff
- Don't retry policy errors - They won't succeed without changes
- Check override URLs - Present Blink URLs to vault owners
- Monitor error rates - Track errors in metrics
- Test error cases - Write tests for each error type
- Provide fallbacks - Gracefully degrade when possible
- Alert on critical errors - Notify on-call for UnauthorizedSignerError
Next Steps
- Best Practices - Production tips
- Code Examples - Real-world usage patterns
- API Reference - Complete method documentation