Skip to main content

Error Handling

Implement robust error handling to create reliable SavvyMoney integrations. This guide covers error types, handling strategies, and user experience best practices.

Error Response Format

All SavvyMoney API errors follow a consistent format:

{
"error": {
"code": "ERROR_CODE",
"message": "Human-readable error description",
"details": [
{
"field": "email",
"message": "Invalid email format"
}
]
},
"meta": {
"requestId": "req_abc123xyz",
"timestamp": "2024-01-15T10:30:00Z"
}
}

Error Categories

Client Errors (4xx)

StatusCodeDescriptionAction
400VALIDATION_ERRORInvalid request parametersFix request and retry
401AUTHENTICATION_ERRORInvalid or expired tokenRefresh token and retry
403AUTHORIZATION_ERRORInsufficient permissionsCheck API scopes
404NOT_FOUNDResource doesn't existVerify resource ID
409CONFLICTResource already existsHandle duplicate
422UNPROCESSABLE_ENTITYSemantic validation errorReview business logic
429RATE_LIMIT_EXCEEDEDToo many requestsImplement backoff

Server Errors (5xx)

StatusCodeDescriptionAction
500INTERNAL_ERRORServer errorRetry with backoff
502BAD_GATEWAYUpstream errorRetry with backoff
503SERVICE_UNAVAILABLEService temporarily downRetry with backoff
504GATEWAY_TIMEOUTRequest timeoutRetry with backoff

Business Logic Errors

CodeDescriptionUser Action
USER_NOT_ENROLLEDUser must enroll firstStart enrollment flow
USER_ALREADY_ENROLLEDDuplicate enrollmentSkip enrollment
CREDIT_FROZENUser's credit is frozenInstruct user to unfreeze
VERIFICATION_FAILEDIdentity verification failedRetry or contact support
VERIFICATION_EXPIREDSession expiredRestart verification

Error Handler Implementation

TypeScript/JavaScript

Error Handler Class
// Define error types
interface SavvyMoneyError {
code: string;
message: string;
details?: Array<{ field: string; message: string }>;
status: number;
requestId?: string;
}

class SavvyMoneyApiError extends Error {
public readonly code: string;
public readonly status: number;
public readonly details?: Array<{ field: string; message: string }>;
public readonly requestId?: string;
public readonly isRetryable: boolean;

constructor(error: SavvyMoneyError) {
super(error.message);
this.name = 'SavvyMoneyApiError';
this.code = error.code;
this.status = error.status;
this.details = error.details;
this.requestId = error.requestId;
this.isRetryable = this.determineRetryable();
}

private determineRetryable(): boolean {
// Retry on server errors and rate limits
if (this.status >= 500) return true;
if (this.status === 429) return true;

// Don't retry client errors
return false;
}
}

// API client with error handling
class SavvyMoneyClient {
private baseUrl: string;
private tokenManager: TokenManager;

async request<T>(
method: string,
path: string,
options: RequestOptions = {}
): Promise<T> {
const token = await this.tokenManager.getToken();

const response = await fetch(`${this.baseUrl}${path}`, {
method,
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
...options.headers
},
body: options.body ? JSON.stringify(options.body) : undefined
});

if (!response.ok) {
const errorData = await response.json();
throw new SavvyMoneyApiError({
...errorData.error,
status: response.status,
requestId: errorData.meta?.requestId
});
}

return response.json();
}
}

Retry Logic

Retry with Exponential Backoff
interface RetryOptions {
maxRetries: number;
baseDelayMs: number;
maxDelayMs: number;
retryableStatuses: number[];
}

const defaultRetryOptions: RetryOptions = {
maxRetries: 3,
baseDelayMs: 1000,
maxDelayMs: 30000,
retryableStatuses: [429, 500, 502, 503, 504]
};

async function withRetry<T>(
fn: () => Promise<T>,
options: Partial<RetryOptions> = {}
): Promise<T> {
const config = { ...defaultRetryOptions, ...options };
let lastError: Error;

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

// Check if error is retryable
if (error instanceof SavvyMoneyApiError) {
if (!error.isRetryable) {
throw error;
}

// Handle rate limiting
if (error.status === 429) {
const retryAfter = parseInt(error.message.match(/(\d+)/)?.[1] || '60');
await sleep(retryAfter * 1000);
continue;
}
}

// Calculate backoff delay
if (attempt < config.maxRetries) {
const delay = Math.min(
config.baseDelayMs * Math.pow(2, attempt),
config.maxDelayMs
);
const jitter = delay * 0.2 * Math.random();
await sleep(delay + jitter);
}
}
}

throw lastError!;
}

function sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}

Python Implementation

Python Error Handling
from dataclasses import dataclass
from typing import Optional, List, Dict, Any
import time
import random

@dataclass
class ErrorDetail:
field: str
message: str

@dataclass
class SavvyMoneyError(Exception):
code: str
message: str
status: int
details: Optional[List[ErrorDetail]] = None
request_id: Optional[str] = None

@property
def is_retryable(self) -> bool:
return self.status >= 500 or self.status == 429


class SavvyMoneyClient:
def __init__(self, base_url: str, client_id: str, client_secret: str):
self.base_url = base_url
self.client_id = client_id
self.client_secret = client_secret
self._token = None
self._token_expires_at = 0

def request(
self,
method: str,
path: str,
body: Optional[Dict[str, Any]] = None,
max_retries: int = 3
) -> Dict[str, Any]:
"""Make an API request with automatic retry."""
last_error = None

for attempt in range(max_retries + 1):
try:
return self._do_request(method, path, body)
except SavvyMoneyError as e:
last_error = e

if not e.is_retryable:
raise

if attempt < max_retries:
if e.status == 429:
# Use Retry-After header if available
delay = 60
else:
delay = min(1000 * (2 ** attempt), 30000) / 1000
delay += delay * 0.2 * random.random()

time.sleep(delay)

raise last_error

def _do_request(
self,
method: str,
path: str,
body: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]:
import requests

token = self._get_token()

response = requests.request(
method=method,
url=f"{self.base_url}{path}",
headers={
"Authorization": f"Bearer {token}",
"Content-Type": "application/json"
},
json=body
)

if not response.ok:
error_data = response.json()
raise SavvyMoneyError(
code=error_data["error"]["code"],
message=error_data["error"]["message"],
status=response.status_code,
details=error_data["error"].get("details"),
request_id=error_data.get("meta", {}).get("requestId")
)

return response.json()

Java Implementation

Java Error Handling
public class SavvyMoneyException extends RuntimeException {
private final String code;
private final int status;
private final List<FieldError> details;
private final String requestId;

public SavvyMoneyException(String code, String message, int status,
List<FieldError> details, String requestId) {
super(message);
this.code = code;
this.status = status;
this.details = details;
this.requestId = requestId;
}

public boolean isRetryable() {
return status >= 500 || status == 429;
}

// Getters...
}

@Service
public class SavvyMoneyClient {

private final RestTemplate restTemplate;
private final TokenManager tokenManager;

@Retryable(
value = SavvyMoneyException.class,
maxAttempts = 3,
backoff = @Backoff(delay = 1000, multiplier = 2, maxDelay = 30000),
exceptionExpression = "#{#root.retryable}"
)
public <T> T request(String method, String path, Object body, Class<T> responseType) {
String token = tokenManager.getToken();

HttpHeaders headers = new HttpHeaders();
headers.setBearerAuth(token);
headers.setContentType(MediaType.APPLICATION_JSON);

HttpEntity<?> entity = new HttpEntity<>(body, headers);

try {
ResponseEntity<T> response = restTemplate.exchange(
baseUrl + path,
HttpMethod.valueOf(method),
entity,
responseType
);
return response.getBody();
} catch (HttpStatusCodeException e) {
throw parseError(e);
}
}

private SavvyMoneyException parseError(HttpStatusCodeException e) {
try {
ErrorResponse error = objectMapper.readValue(
e.getResponseBodyAsString(),
ErrorResponse.class
);
return new SavvyMoneyException(
error.getError().getCode(),
error.getError().getMessage(),
e.getStatusCode().value(),
error.getError().getDetails(),
error.getMeta().getRequestId()
);
} catch (Exception ex) {
return new SavvyMoneyException(
"UNKNOWN_ERROR",
e.getMessage(),
e.getStatusCode().value(),
null,
null
);
}
}
}

Handling Specific Errors

Authentication Errors

Handle Authentication Errors
async function handleAuthError(error: SavvyMoneyApiError) {
if (error.code === 'AUTHENTICATION_ERROR') {
// Token expired - refresh and retry
tokenManager.invalidate();

try {
await tokenManager.getToken();
return true; // Signal to retry
} catch (refreshError) {
// Refresh failed - re-authenticate
throw new Error('Session expired. Please log in again.');
}
}
return false;
}

Validation Errors

Handle Validation Errors
function handleValidationError(error: SavvyMoneyApiError): ValidationResult {
if (error.code !== 'VALIDATION_ERROR' || !error.details) {
return { valid: false, errors: { _general: [error.message] } };
}

const fieldErrors: Record<string, string[]> = {};

for (const detail of error.details) {
if (!fieldErrors[detail.field]) {
fieldErrors[detail.field] = [];
}
fieldErrors[detail.field].push(detail.message);
}

return { valid: false, errors: fieldErrors };
}

// Usage in a form
async function submitEnrollment(formData: EnrollmentData) {
try {
return await savvyClient.enrollUser(formData);
} catch (error) {
if (error instanceof SavvyMoneyApiError) {
const validation = handleValidationError(error);
setFormErrors(validation.errors);
}
throw error;
}
}

Rate Limit Errors

Handle Rate Limits
async function handleRateLimit<T>(
fn: () => Promise<T>,
maxWait: number = 120000
): Promise<T> {
try {
return await fn();
} catch (error) {
if (error instanceof SavvyMoneyApiError && error.status === 429) {
// Parse retry-after from error message
const retryAfter = parseInt(error.message.match(/(\d+)/)?.[1] || '60');
const waitMs = retryAfter * 1000;

if (waitMs > maxWait) {
throw new Error('Rate limit exceeded. Please try again later.');
}

console.log(`Rate limited. Waiting ${retryAfter} seconds...`);
await sleep(waitMs);

return fn();
}
throw error;
}
}

Business Logic Errors

Handle Business Logic Errors
async function getCreditScoreWithFallback(userId: string): Promise<CreditScoreResult> {
try {
const score = await savvyClient.getCreditScore(userId);
return { success: true, data: score };
} catch (error) {
if (!(error instanceof SavvyMoneyApiError)) {
throw error;
}

switch (error.code) {
case 'USER_NOT_ENROLLED':
return {
success: false,
action: 'enroll',
message: 'Please complete enrollment to view your credit score.'
};

case 'CREDIT_FROZEN':
return {
success: false,
action: 'unfreeze',
message: 'Your credit file is frozen. Please unfreeze it to continue.'
};

case 'VERIFICATION_FAILED':
return {
success: false,
action: 'verify',
message: 'We couldn\'t verify your identity. Please try again.'
};

default:
throw error;
}
}
}

User Experience

User-Friendly Error Messages

Map technical errors to user-friendly messages:

Error Message Mapping
const errorMessages: Record<string, string> = {
// Authentication
'AUTHENTICATION_ERROR': 'Your session has expired. Please sign in again.',
'AUTHORIZATION_ERROR': 'You don\'t have permission to access this feature.',

// Validation
'VALIDATION_ERROR': 'Please check your information and try again.',

// Business Logic
'USER_NOT_ENROLLED': 'Complete enrollment to access your credit score.',
'CREDIT_FROZEN': 'Your credit is frozen. Visit the credit bureaus to unfreeze.',
'VERIFICATION_FAILED': 'We couldn\'t verify your identity. Please try again.',
'VERIFICATION_EXPIRED': 'Your verification session expired. Please start over.',

// Server Errors
'INTERNAL_ERROR': 'Something went wrong. Please try again in a few minutes.',
'SERVICE_UNAVAILABLE': 'Service temporarily unavailable. Please try again later.',
'RATE_LIMIT_EXCEEDED': 'Too many requests. Please wait a moment and try again.',

// Default
'UNKNOWN_ERROR': 'An unexpected error occurred. Please try again.'
};

function getUserMessage(error: SavvyMoneyApiError): string {
return errorMessages[error.code] || errorMessages['UNKNOWN_ERROR'];
}

Error UI Components

React Error Component
interface ErrorDisplayProps {
error: SavvyMoneyApiError;
onRetry?: () => void;
onDismiss?: () => void;
}

function ErrorDisplay({ error, onRetry, onDismiss }: ErrorDisplayProps) {
const message = getUserMessage(error);
const canRetry = error.isRetryable;

return (
<div className="error-container" role="alert">
<div className="error-icon">⚠️</div>
<div className="error-content">
<h3 className="error-title">Something went wrong</h3>
<p className="error-message">{message}</p>

{error.requestId && (
<p className="error-id">Reference: {error.requestId}</p>
)}
</div>

<div className="error-actions">
{canRetry && onRetry && (
<button onClick={onRetry} className="btn-retry">
Try Again
</button>
)}
{onDismiss && (
<button onClick={onDismiss} className="btn-dismiss">
Dismiss
</button>
)}
</div>
</div>
);
}

Logging & Monitoring

Error Logging

Structured Error Logging
interface ErrorLogEntry {
timestamp: string;
level: 'error' | 'warn';
errorCode: string;
errorMessage: string;
status: number;
requestId?: string;
userId?: string;
endpoint: string;
duration?: number;
stack?: string;
}

function logError(error: SavvyMoneyApiError, context: Partial<ErrorLogEntry>) {
const entry: ErrorLogEntry = {
timestamp: new Date().toISOString(),
level: error.status >= 500 ? 'error' : 'warn',
errorCode: error.code,
errorMessage: error.message,
status: error.status,
requestId: error.requestId,
...context
};

// Send to logging service
logger.log(entry);

// Alert on critical errors
if (error.status >= 500) {
alertService.notify({
severity: 'high',
message: `SavvyMoney API error: ${error.code}`,
details: entry
});
}
}

Error Metrics

Track error rates and patterns:

Error Metrics
class ErrorMetrics {
private errors: Map<string, number[]> = new Map();

record(errorCode: string) {
const now = Date.now();
if (!this.errors.has(errorCode)) {
this.errors.set(errorCode, []);
}
this.errors.get(errorCode)!.push(now);
this.cleanup(errorCode);
}

private cleanup(errorCode: string) {
const cutoff = Date.now() - 3600000; // 1 hour
const timestamps = this.errors.get(errorCode)!;
this.errors.set(errorCode, timestamps.filter(t => t > cutoff));
}

getRate(errorCode: string, windowMs: number = 300000): number {
const cutoff = Date.now() - windowMs;
const timestamps = this.errors.get(errorCode) || [];
return timestamps.filter(t => t > cutoff).length;
}

getTopErrors(limit: number = 10): Array<{ code: string; count: number }> {
const counts = Array.from(this.errors.entries())
.map(([code, timestamps]) => ({ code, count: timestamps.length }))
.sort((a, b) => b.count - a.count)
.slice(0, limit);

return counts;
}
}

Error Handling Checklist

  • Custom error class with proper typing
  • Retry logic with exponential backoff
  • Rate limit handling with Retry-After
  • Authentication error recovery
  • Validation error field mapping
  • User-friendly error messages
  • Error UI components
  • Structured error logging
  • Error metrics tracking
  • Alerting for critical errors

Next Steps