Error Handling
Comprehensive error handling is essential for robust integrations with Azotte. This guide covers error types, handling strategies, and best practices for resilient applications.
Error Types
HTTP Status Codes
Azotte uses standard HTTP status codes to indicate the outcome of API requests:
| Status Code | Description | Action Required |
|---|---|---|
| 200 | Success | Continue normally |
| 400 | Bad Request | Fix request parameters |
| 401 | Unauthorized | Check API key |
| 403 | Forbidden | Check permissions |
| 404 | Not Found | Verify resource exists |
| 409 | Conflict | Handle duplicate/conflict |
| 422 | Validation Error | Fix validation issues |
| 429 | Rate Limited | Implement backoff |
| 500 | Server Error | Retry with backoff |
| 503 | Service Unavailable | Retry later |
Error Response Format
{
"error": {
"type": "validation_error",
"code": "invalid_email",
"message": "The email address provided is invalid",
"details": {
"field": "email",
"value": "invalid-email",
"constraint": "valid_email_format"
},
"request_id": "req_1234567890abcdef"
}
}
Error Categories
Validation Errors
interface ValidationError {
type: 'validation_error';
code: string;
message: string;
details: {
field: string;
value: any;
constraint: string;
expected?: string;
};
}
// Common validation error codes
const validationCodes = {
'required_field': 'Field is required but not provided',
'invalid_format': 'Field format is invalid',
'out_of_range': 'Value is outside acceptable range',
'invalid_enum': 'Value not in allowed enum values',
'duplicate_value': 'Value already exists'
};
Authentication Errors
interface AuthError {
type: 'authentication_error';
code: 'invalid_api_key' | 'expired_token' | 'insufficient_permissions';
message: string;
details: {
api_key_hint?: string;
required_permissions?: string[];
};
}
// Example authentication error
{
"error": {
"type": "authentication_error",
"code": "invalid_api_key",
"message": "The API key provided is invalid",
"details": {
"api_key_hint": "sk_live_****1234"
}
}
}
Business Logic Errors
interface BusinessError {
type: 'business_error';
code: string;
message: string;
details: {
entity_id?: string;
entity_type?: string;
business_rule?: string;
};
}
// Common business error codes
const businessCodes = {
'insufficient_balance': 'Customer has insufficient account balance',
'subscription_already_active': 'Customer already has an active subscription',
'plan_not_available': 'The requested plan is not available',
'payment_method_declined': 'Payment method was declined',
'duplicate_subscription': 'Subscription already exists for this customer'
};
Error Handling Strategies
Retry Logic
class RetryHandler {
constructor(maxRetries = 3, baseDelay = 1000) {
this.maxRetries = maxRetries;
this.baseDelay = baseDelay;
}
async executeWithRetry(fn, retryableErrors = [500, 502, 503, 504]) {
let lastError;
for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
lastError = error;
// Don't retry non-retryable errors
if (!this.isRetryable(error, retryableErrors)) {
throw error;
}
// Don't retry on last attempt
if (attempt === this.maxRetries) {
throw error;
}
// Calculate delay with exponential backoff
const delay = this.baseDelay * Math.pow(2, attempt);
const jitter = Math.random() * 0.1 * delay;
await this.sleep(delay + jitter);
}
}
throw lastError;
}
isRetryable(error, retryableErrors) {
return retryableErrors.includes(error.status) ||
error.type === 'network_error' ||
error.type === 'timeout_error';
}
sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
// Usage
const retryHandler = new RetryHandler();
const result = await retryHandler.executeWithRetry(async () => {
return await azotte.subscriptions.create(subscriptionData);
});
Circuit Breaker Pattern
class CircuitBreaker {
constructor(options = {}) {
this.failureThreshold = options.failureThreshold || 5;
this.timeout = options.timeout || 60000;
this.monitoringPeriod = options.monitoringPeriod || 10000;
this.failureCount = 0;
this.lastFailureTime = null;
this.state = 'CLOSED'; // CLOSED, OPEN, HALF_OPEN
this.nextAttempt = Date.now();
}
async call(fn) {
if (this.state === 'OPEN') {
if (Date.now() < this.nextAttempt) {
throw new Error('Circuit breaker is OPEN');
}
// Try to transition to HALF_OPEN
this.state = 'HALF_OPEN';
}
try {
const result = await fn();
this.onSuccess();
return result;
} catch (error) {
this.onFailure();
throw error;
}
}
onSuccess() {
this.failureCount = 0;
this.state = 'CLOSED';
this.nextAttempt = Date.now();
}
onFailure() {
this.failureCount++;
this.lastFailureTime = Date.now();
if (this.failureCount >= this.failureThreshold) {
this.state = 'OPEN';
this.nextAttempt = Date.now() + this.timeout;
}
}
}
Graceful Degradation
class ApiService {
constructor() {
this.circuitBreaker = new CircuitBreaker();
this.cache = new Map();
}
async getCustomer(customerId) {
try {
return await this.circuitBreaker.call(async () => {
const customer = await azotte.customers.retrieve(customerId);
this.cache.set(`customer_${customerId}`, customer);
return customer;
});
} catch (error) {
// Fallback to cached data
const cachedCustomer = this.cache.get(`customer_${customerId}`);
if (cachedCustomer) {
console.warn('Using cached customer data due to API error');
return { ...cachedCustomer, _cached: true };
}
// Return minimal data to keep application functional
return {
id: customerId,
name: 'Unknown Customer',
email: 'unknown@example.com',
_error: true
};
}
}
}
SDK Error Handling
JavaScript/TypeScript
import { Azotte, AzotteError } from '@azotte/sdk';
const azotte = new Azotte({
apiKey: process.env.AZOTTE_API_KEY,
// Enable automatic retries for network errors
retryOptions: {
maxRetries: 3,
retryDelay: 'exponential'
}
});
try {
const subscription = await azotte.subscriptions.create({
customer_id: 'cus_123',
plan_id: 'plan_basic'
});
} catch (error) {
if (error instanceof AzotteError) {
switch (error.type) {
case 'validation_error':
console.error('Validation failed:', error.details);
// Show user-friendly error message
break;
case 'authentication_error':
console.error('Authentication failed:', error.message);
// Refresh API key or redirect to login
break;
case 'rate_limit_error':
console.error('Rate limited, retry after:', error.retry_after);
// Implement exponential backoff
break;
case 'business_error':
console.error('Business rule violation:', error.code);
// Handle specific business logic
break;
default:
console.error('Unknown API error:', error);
// Generic error handling
}
} else {
// Network or other errors
console.error('Network error:', error);
}
}
Error Context Logging
class ErrorLogger {
static logError(error, context = {}) {
const errorInfo = {
timestamp: new Date().toISOString(),
error_type: error.type || 'unknown',
error_code: error.code,
message: error.message,
request_id: error.request_id,
context: {
user_id: context.userId,
tenant_id: context.tenantId,
endpoint: context.endpoint,
request_data: context.requestData
},
stack_trace: error.stack
};
// Log to monitoring service
console.error('API Error:', JSON.stringify(errorInfo, null, 2));
// Send to error tracking service
if (typeof window !== 'undefined' && window.Sentry) {
window.Sentry.captureException(error, {
contexts: { azotte_api: errorInfo }
});
}
}
}
// Usage
try {
await azotte.customers.create(customerData);
} catch (error) {
ErrorLogger.logError(error, {
userId: currentUser.id,
tenantId: currentTenant.id,
endpoint: '/customers',
requestData: customerData
});
}
Webhook Error Handling
Webhook Retry Logic
app.post('/webhooks/azotte', express.raw({type: 'application/json'}), (req, res) => {
const signature = req.headers['azotte-signature'];
let event;
try {
event = azotte.webhooks.constructEvent(req.body, signature);
} catch (error) {
console.error('Webhook signature verification failed:', error.message);
return res.status(400).send('Invalid signature');
}
try {
processWebhookEvent(event);
res.status(200).send('OK');
} catch (error) {
console.error('Webhook processing failed:', error);
// Return 500 to trigger Azotte's retry logic
res.status(500).send('Processing failed');
}
});
async function processWebhookEvent(event) {
try {
switch (event.type) {
case 'subscription.created':
await handleSubscriptionCreated(event.data);
break;
case 'payment.succeeded':
await handlePaymentSucceeded(event.data);
break;
default:
console.log('Unhandled event type:', event.type);
}
} catch (error) {
// Log error with event context
ErrorLogger.logError(error, {
event_type: event.type,
event_id: event.id,
webhook_endpoint: '/webhooks/azotte'
});
throw error;
}
}
Webhook Dead Letter Queue
class WebhookProcessor {
constructor() {
this.maxRetries = 3;
this.deadLetterQueue = [];
}
async processWebhook(event, attempt = 1) {
try {
await this.handleEvent(event);
return { success: true };
} catch (error) {
if (attempt < this.maxRetries) {
// Retry with exponential backoff
const delay = Math.pow(2, attempt) * 1000;
setTimeout(() => {
this.processWebhook(event, attempt + 1);
}, delay);
return { success: false, retrying: true };
} else {
// Move to dead letter queue
this.deadLetterQueue.push({
event,
error: error.message,
failed_at: new Date(),
attempts: attempt
});
return { success: false, dead_letter: true };
}
}
}
async reprocessDeadLetters() {
const events = [...this.deadLetterQueue];
this.deadLetterQueue = [];
for (const item of events) {
await this.processWebhook(item.event, 1);
}
}
}
Error Monitoring & Alerting
Error Metrics
class ErrorMetrics {
constructor() {
this.errorCounts = new Map();
this.errorRates = new Map();
}
recordError(error, context = {}) {
const errorKey = `${error.type}_${error.code}`;
const count = this.errorCounts.get(errorKey) || 0;
this.errorCounts.set(errorKey, count + 1);
// Calculate error rate per minute
const now = Math.floor(Date.now() / 60000);
const rateKey = `${errorKey}_${now}`;
const rate = this.errorRates.get(rateKey) || 0;
this.errorRates.set(rateKey, rate + 1);
// Alert if error rate exceeds threshold
if (rate > 10) {
this.sendAlert(`High error rate for ${errorKey}: ${rate}/min`);
}
}
sendAlert(message) {
// Send to monitoring service
console.error('ALERT:', message);
}
getErrorSummary() {
return {
total_errors: Array.from(this.errorCounts.values())
.reduce((sum, count) => sum + count, 0),
error_breakdown: Object.fromEntries(this.errorCounts),
top_errors: Array.from(this.errorCounts.entries())
.sort(([,a], [,b]) => b - a)
.slice(0, 5)
};
}
}
Best Practices
Error Response Handling
// Don't expose internal errors to users
const handleApiError = (error) => {
if (error.type === 'validation_error') {
return {
message: 'Please check your input and try again.',
field_errors: error.details
};
}
if (error.type === 'authentication_error') {
return {
message: 'Authentication failed. Please log in again.',
redirect: '/login'
};
}
// Generic error for unknown issues
return {
message: 'Something went wrong. Please try again later.',
support_id: error.request_id
};
};
Idempotency
// Use idempotency keys for critical operations
const createSubscription = async (data) => {
const idempotencyKey = `create_sub_${data.customer_id}_${Date.now()}`;
try {
return await azotte.subscriptions.create(data, {
idempotency_key: idempotencyKey
});
} catch (error) {
if (error.code === 'duplicate_operation') {
// Operation already completed successfully
return error.original_response;
}
throw error;
}
};
Health Checks
class HealthChecker {
async checkAzotteApi() {
try {
await azotte.ping();
return { status: 'healthy', latency: Date.now() - start };
} catch (error) {
return { status: 'unhealthy', error: error.message };
}
}
async getSystemHealth() {
const checks = await Promise.allSettled([
this.checkAzotteApi(),
this.checkDatabase(),
this.checkRedis()
]);
return {
overall: checks.every(c => c.status === 'fulfilled'),
services: checks.map(c => c.value || c.reason)
};
}
}
Testing Error Scenarios
Unit Tests
describe('Subscription Creation', () => {
it('should handle validation errors gracefully', async () => {
const invalidData = { customer_id: null };
await expect(
subscriptionService.create(invalidData)
).rejects.toMatchObject({
type: 'validation_error',
code: 'required_field'
});
});
it('should retry on network errors', async () => {
const mockError = new Error('Network timeout');
mockError.type = 'network_error';
azotte.subscriptions.create = jest.fn()
.mockRejectedValueOnce(mockError)
.mockResolvedValueOnce({ id: 'sub_123' });
const result = await subscriptionService.create(validData);
expect(result.id).toBe('sub_123');
expect(azotte.subscriptions.create).toHaveBeenCalledTimes(2);
});
});
Next Steps
- Learn about Rate Limiting
- Understand Tenant Context
- Explore Authentication