Skip to main content

Error Handling

This guide covers error handling strategies and best practices for the SatsTerminal Borrow SDK.

Error Types Overview

The SDK provides typed errors for precise handling:
Error ClassWhen Thrown
WalletNotConnectedErrorWallet operations without connected wallet
SmartAccountErrorSmart account initialization/operation failures
ApiErrorAPI request failures
ConfigValidationErrorInvalid SDK configuration
QuoteErrorQuote-related failures
WorkflowErrorWorkflow execution failures

Basic Error Handling

import {
  BorrowSDK,
  BorrowSDKError,
  WalletNotConnectedError,
  SmartAccountError,
  ApiError,
  ConfigValidationError,
  QuoteError,
  WorkflowError
} from '@satsterminal-sdk/borrow';

try {
  await sdk.getLoan({
    collateralBTC: 0.1,
    loanAmountUSD: 5000
  });
} catch (error) {
  if (error instanceof WalletNotConnectedError) {
    // Handle wallet not connected
    showConnectWalletPrompt();
  } else if (error instanceof SmartAccountError) {
    // Handle smart account issues
    console.error('Smart account error:', error.message);
  } else if (error instanceof ApiError) {
    // Handle API errors
    handleApiError(error);
  } else if (error instanceof QuoteError) {
    // Handle quote errors
    showMessage('No quotes available. Try different parameters.');
  } else if (error instanceof WorkflowError) {
    // Handle workflow errors
    console.error('Workflow failed:', error.workflowId);
  } else {
    // Handle unknown errors
    console.error('Unexpected error:', error);
  }
}

Handling API Errors

API errors include HTTP status codes for precise handling:
async function handleApiError(error: ApiError) {
  switch (error.statusCode) {
    case 400:
      // Bad request - validation error
      showValidationError(error.message);
      break;

    case 401:
      // Unauthorized - invalid API key
      showError('Invalid API key. Please check your configuration.');
      break;

    case 403:
      // Forbidden
      showError('Access denied.');
      break;

    case 404:
      // Not found
      showError('Resource not found.');
      break;

    case 429:
      // Rate limited
      showError('Too many requests. Please wait.');
      await delay(5000);
      // Retry operation
      break;

    case 500:
    case 502:
    case 503:
    case 504:
      // Server errors
      showError('Server error. Please try again later.');
      break;

    default:
      showError(`Error: ${error.message}`);
  }
}

Retry Strategies

Automatic Retry Configuration

const sdk = new BorrowSDK({
  // ...config
  retryConfig: {
    maxRetries: 3,
    retryDelay: 1000,
    retryableStatusCodes: [408, 429, 500, 502, 503, 504]
  }
});

Manual Retry with Backoff

async function retryWithBackoff<T>(
  operation: () => Promise<T>,
  maxRetries = 3,
  baseDelay = 1000
): Promise<T> {
  let lastError: Error | undefined;

  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      return await operation();
    } catch (error) {
      lastError = error as Error;

      // Check if error is retryable
      if (error instanceof ApiError) {
        const retryable = [408, 429, 500, 502, 503, 504];
        if (!retryable.includes(error.statusCode || 0)) {
          throw error; // Not retryable
        }
      } else if (!(error instanceof BorrowSDKError)) {
        throw error; // Unknown error, don't retry
      }

      // Exponential backoff
      const delay = baseDelay * Math.pow(2, attempt);
      console.log(`Retry ${attempt + 1}/${maxRetries} in ${delay}ms...`);
      await new Promise(r => setTimeout(r, delay));
    }
  }

  throw lastError;
}

// Usage
const quotes = await retryWithBackoff(() => sdk.getQuotes(params));

Workflow Error Handling

Workflows have their own error handling through callbacks:
await sdk.getLoan({
  collateralBTC: 0.1,
  loanAmountUSD: 5000,

  onError: (error) => {
    // Error during workflow execution
    console.error('Workflow error:', error);

    if (error.includes('timeout')) {
      showError('Operation timed out. Your deposit may still be processing.');
    } else if (error.includes('deposit')) {
      showError('Deposit not received. Please check your transaction.');
    } else if (error.includes('bridge')) {
      showError('Bridge operation failed. Please contact support.');
    } else {
      showError(`Operation failed: ${error}`);
    }
  }
});

Session Error Recovery

async function executeWithSession<T>(
  sdk: BorrowSDK,
  operation: () => Promise<T>
): Promise<T> {
  try {
    return await operation();
  } catch (error) {
    if (error instanceof SmartAccountError) {
      if (error.message.includes('session') ||
          error.message.includes('expired')) {
        // Session expired, refresh it
        console.log('Session expired, refreshing...');
        await sdk.setup();
        return await operation(); // Retry
      }
    }
    throw error;
  }
}

// Usage
const history = await executeWithSession(sdk, () =>
  sdk.getLoanHistory({ status: 'active' })
);

Global Error Handler

class SDKErrorHandler {
  private sdk: BorrowSDK;
  private onError?: (error: BorrowSDKError) => void;
  private onSessionExpired?: () => void;

  constructor(sdk: BorrowSDK) {
    this.sdk = sdk;
  }

  setErrorHandler(handler: (error: BorrowSDKError) => void) {
    this.onError = handler;
  }

  setSessionExpiredHandler(handler: () => void) {
    this.onSessionExpired = handler;
  }

  async execute<T>(operation: () => Promise<T>): Promise<T> {
    try {
      return await operation();
    } catch (error) {
      if (error instanceof BorrowSDKError) {
        this.handleSDKError(error);
        throw error;
      }
      throw error;
    }
  }

  private handleSDKError(error: BorrowSDKError) {
    // Log error
    console.error('SDK Error:', error.toJSON());

    // Check for session expiry
    if (error instanceof SmartAccountError &&
        error.message.includes('session')) {
      this.onSessionExpired?.();
    }

    // Call global handler
    this.onError?.(error);
  }
}

// Usage
const errorHandler = new SDKErrorHandler(sdk);

errorHandler.setErrorHandler((error) => {
  // Send to error tracking service
  errorTracker.capture(error);
});

errorHandler.setSessionExpiredHandler(() => {
  // Redirect to login
  router.push('/connect');
});

// Execute operations
const quotes = await errorHandler.execute(() => sdk.getQuotes(params));

Error Logging

function logError(error: unknown, context: Record<string, any> = {}) {
  if (error instanceof BorrowSDKError) {
    const errorData = {
      type: error.constructor.name,
      code: error.code,
      message: error.message,
      context: { ...error.context, ...context },
      timestamp: new Date().toISOString(),
      stack: error.stack
    };

    // Log locally
    console.error('SDK Error:', JSON.stringify(errorData, null, 2));

    // Send to monitoring service
    monitoring.logError(errorData);

  } else if (error instanceof Error) {
    console.error('Error:', error.message, context);
    monitoring.logError({
      type: 'Error',
      message: error.message,
      context,
      timestamp: new Date().toISOString(),
      stack: error.stack
    });
  } else {
    console.error('Unknown error:', error);
  }
}

User-Friendly Error Messages

function getUserMessage(error: unknown): string {
  if (error instanceof WalletNotConnectedError) {
    return 'Please connect your wallet to continue.';
  }

  if (error instanceof SmartAccountError) {
    if (error.message.includes('session')) {
      return 'Your session has expired. Please reconnect.';
    }
    return 'There was a problem with your account. Please try again.';
  }

  if (error instanceof ApiError) {
    switch (error.statusCode) {
      case 401:
        return 'Authentication failed. Please check your API key.';
      case 429:
        return 'Too many requests. Please wait a moment and try again.';
      case 500:
        return 'Server error. Our team has been notified.';
      default:
        return 'Something went wrong. Please try again.';
    }
  }

  if (error instanceof QuoteError) {
    return 'No quotes available for these parameters. Try adjusting your loan amount or collateral.';
  }

  if (error instanceof WorkflowError) {
    return 'The operation could not be completed. Please check your transaction status.';
  }

  if (error instanceof ConfigValidationError) {
    return 'Configuration error. Please contact support.';
  }

  return 'An unexpected error occurred. Please try again.';
}

Best Practices

1. Always Catch Errors

// Bad
await sdk.getLoan(options);

// Good
try {
  await sdk.getLoan(options);
} catch (error) {
  handleError(error);
}

2. Use Type Guards

function isBorrowSDKError(error: unknown): error is BorrowSDKError {
  return error instanceof BorrowSDKError;
}

function isApiError(error: unknown): error is ApiError {
  return error instanceof ApiError;
}

3. Provide Context

try {
  await sdk.repay(loanId, amount);
} catch (error) {
  logError(error, {
    operation: 'repay',
    loanId,
    amount,
    userId: currentUser.id
  });
  throw error;
}

4. Don’t Swallow Errors

// Bad
try {
  await sdk.setup();
} catch (error) {
  // Silent failure
}

// Good
try {
  await sdk.setup();
} catch (error) {
  logError(error);
  showUserError(getUserMessage(error));
  // Optionally re-throw
}

5. Handle Workflow Errors Separately

await sdk.getLoan({
  // ...options
  onError: (error) => {
    // Workflow-specific error handling
    handleWorkflowError(error);
  }
}).catch((error) => {
  // Setup/initialization errors
  handleSetupError(error);
});