Skip to main content

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 CodeDescriptionAction Required
200SuccessContinue normally
400Bad RequestFix request parameters
401UnauthorizedCheck API key
403ForbiddenCheck permissions
404Not FoundVerify resource exists
409ConflictHandle duplicate/conflict
422Validation ErrorFix validation issues
429Rate LimitedImplement backoff
500Server ErrorRetry with backoff
503Service UnavailableRetry 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