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)
| Status | Code | Description | Action |
|---|---|---|---|
| 400 | VALIDATION_ERROR | Invalid request parameters | Fix request and retry |
| 401 | AUTHENTICATION_ERROR | Invalid or expired token | Refresh token and retry |
| 403 | AUTHORIZATION_ERROR | Insufficient permissions | Check API scopes |
| 404 | NOT_FOUND | Resource doesn't exist | Verify resource ID |
| 409 | CONFLICT | Resource already exists | Handle duplicate |
| 422 | UNPROCESSABLE_ENTITY | Semantic validation error | Review business logic |
| 429 | RATE_LIMIT_EXCEEDED | Too many requests | Implement backoff |
Server Errors (5xx)
| Status | Code | Description | Action |
|---|---|---|---|
| 500 | INTERNAL_ERROR | Server error | Retry with backoff |
| 502 | BAD_GATEWAY | Upstream error | Retry with backoff |
| 503 | SERVICE_UNAVAILABLE | Service temporarily down | Retry with backoff |
| 504 | GATEWAY_TIMEOUT | Request timeout | Retry with backoff |
Business Logic Errors
| Code | Description | User Action |
|---|---|---|
USER_NOT_ENROLLED | User must enroll first | Start enrollment flow |
USER_ALREADY_ENROLLED | Duplicate enrollment | Skip enrollment |
CREDIT_FROZEN | User's credit is frozen | Instruct user to unfreeze |
VERIFICATION_FAILED | Identity verification failed | Retry or contact support |
VERIFICATION_EXPIRED | Session expired | Restart 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
- Implement Security Best Practices
- Optimize Performance
- Configure Webhooks for event handling