Saltar al contenido principal

Error Handling

Proper error handling is crucial for building robust integrations with the Billoget API. This guide covers all error types, status codes, and best practices for handling them gracefully.

🚨 Error Response Format​

All API errors follow a consistent JSON structure:

{
"error": "error_code",
"message": "Human-readable error description",
"details": {
"field": "Additional context about the error",
"code": "SPECIFIC_ERROR_CODE"
},
"timestamp": "2024-01-15T10:30:00Z",
"requestId": "req_1234567890abcdef"
}

Error Response Fields​

FieldTypeDescription
errorstringMachine-readable error identifier
messagestringHuman-readable error description
detailsobjectAdditional error context (optional)
timestampstringISO 8601 timestamp of the error
requestIdstringUnique request identifier for debugging

πŸ“Š HTTP Status Codes​

2xx Success​

CodeStatusDescription
200OKRequest successful
201CreatedResource created successfully
204No ContentRequest successful, no content returned

4xx Client Errors​

CodeStatusDescription
400Bad RequestInvalid request format or parameters
401UnauthorizedMissing or invalid authentication
403ForbiddenInsufficient permissions
404Not FoundResource not found
409ConflictResource conflict (duplicate, etc.)
422Unprocessable EntityValidation errors
429Too Many RequestsRate limit exceeded

5xx Server Errors​

CodeStatusDescription
500Internal Server ErrorUnexpected server error
502Bad GatewayUpstream service error
503Service UnavailableService temporarily unavailable
504Gateway TimeoutRequest timeout

πŸ” Authentication Errors​

401 Unauthorized​

Missing API Key:

{
"error": "unauthorized",
"message": "Missing Authorization header",
"details": {
"expected": "Authorization: Bearer bk_live_..."
}
}

Invalid API Key:

{
"error": "invalid_api_key",
"message": "Invalid API key format or key not found",
"details": {
"keyPrefix": "bk_live_1234"
}
}

Expired API Key:

{
"error": "api_key_expired",
"message": "API key has expired",
"details": {
"expiredAt": "2024-01-01T00:00:00Z"
}
}

403 Forbidden​

Insufficient Scope:

{
"error": "insufficient_scope",
"message": "API key does not have required scope: WRITE_BUDGETS",
"details": {
"required": "WRITE_BUDGETS",
"available": ["READ_BUDGETS", "READ_CUSTOMERS"]
}
}

IP Not Allowed:

{
"error": "ip_not_allowed",
"message": "Request from unauthorized IP address",
"details": {
"clientIp": "203.0.113.100",
"allowedIps": ["192.168.1.100", "10.0.0.50"]
}
}

πŸ“ Validation Errors​

422 Unprocessable Entity​

Field Validation:

{
"error": "validation_error",
"message": "Request validation failed",
"details": {
"errors": [
{
"field": "email",
"code": "INVALID_FORMAT",
"message": "Invalid email format"
},
{
"field": "total",
"code": "MIN_VALUE",
"message": "Total must be greater than 0"
}
]
}
}

Missing Required Fields:

{
"error": "missing_required_fields",
"message": "Required fields are missing",
"details": {
"missing": ["name", "email"],
"provided": ["phone", "address"]
}
}

🚦 Rate Limiting Errors​

429 Too Many Requests​

{
"error": "rate_limit_exceeded",
"message": "Rate limit exceeded. Try again in 3600 seconds",
"details": {
"limit": 1000,
"remaining": 0,
"resetAt": "2024-01-15T11:30:00Z",
"retryAfter": 3600
}
}

πŸ” Resource Errors​

404 Not Found​

Resource Not Found:

{
"error": "resource_not_found",
"message": "Budget with ID 'budget_123' not found",
"details": {
"resource": "budget",
"id": "budget_123"
}
}

Endpoint Not Found:

{
"error": "endpoint_not_found",
"message": "Endpoint '/api/public/invalid' not found",
"details": {
"path": "/api/public/invalid",
"method": "GET"
}
}

409 Conflict​

Duplicate Resource:

{
"error": "duplicate_resource",
"message": "Customer with email 'john@example.com' already exists",
"details": {
"field": "email",
"value": "john@example.com",
"existingId": "customer_456"
}
}

πŸ› οΈ Error Handling Patterns​

1. Basic Error Handling​

async function makeApiRequest(endpoint, options = {}) {
try {
const response = await fetch(`https://api.billoget.com${endpoint}`, {
...options,
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
...options.headers,
},
});

// Check if response is successful
if (!response.ok) {
const error = await response.json();
throw new APIError(error, response.status);
}

return await response.json();
} catch (error) {
if (error instanceof APIError) {
throw error;
}
// Handle network errors
throw new APIError(
{
error: "network_error",
message: "Network request failed",
},
0
);
}
}

class APIError extends Error {
constructor(errorData, statusCode) {
super(errorData.message);
this.name = "APIError";
this.error = errorData.error;
this.statusCode = statusCode;
this.details = errorData.details;
this.requestId = errorData.requestId;
}
}

2. Comprehensive Error Handler​

class BillogetErrorHandler {
static handle(error) {
if (!(error instanceof APIError)) {
console.error("Unexpected error:", error);
return this.handleGenericError(error);
}

switch (error.statusCode) {
case 400:
return this.handleBadRequest(error);
case 401:
return this.handleUnauthorized(error);
case 403:
return this.handleForbidden(error);
case 404:
return this.handleNotFound(error);
case 409:
return this.handleConflict(error);
case 422:
return this.handleValidationError(error);
case 429:
return this.handleRateLimit(error);
case 500:
case 502:
case 503:
case 504:
return this.handleServerError(error);
default:
return this.handleGenericError(error);
}
}

static handleUnauthorized(error) {
console.error("Authentication failed:", error.message);

switch (error.error) {
case "unauthorized":
return "Please check your API key";
case "invalid_api_key":
return "Invalid API key. Please verify your key";
case "api_key_expired":
return "API key has expired. Please generate a new one";
default:
return "Authentication error";
}
}

static handleForbidden(error) {
console.error("Permission denied:", error.message);

switch (error.error) {
case "insufficient_scope":
return `Missing permission: ${error.details.required}`;
case "ip_not_allowed":
return "Request from unauthorized IP address";
default:
return "Permission denied";
}
}

static handleValidationError(error) {
console.error("Validation failed:", error.details);

if (error.details.errors) {
const fieldErrors = error.details.errors
.map((err) => `${err.field}: ${err.message}`)
.join(", ");
return `Validation errors: ${fieldErrors}`;
}

return "Request validation failed";
}

static handleRateLimit(error) {
const retryAfter = error.details.retryAfter;
console.warn(`Rate limited. Retry after ${retryAfter} seconds`);
return `Too many requests. Please wait ${retryAfter} seconds`;
}

static handleServerError(error) {
console.error(
"Server error:",
error.message,
"Request ID:",
error.requestId
);
return "Server error. Please try again later";
}

static handleGenericError(error) {
console.error("Unexpected error:", error);
return "An unexpected error occurred";
}
}

3. Retry Logic with Exponential Backoff​

class RetryableRequest {
constructor(maxRetries = 3, baseDelay = 1000) {
this.maxRetries = maxRetries;
this.baseDelay = baseDelay;
}

async execute(requestFn) {
let lastError;

for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
try {
return await requestFn();
} catch (error) {
lastError = error;

// Don't retry on client errors (4xx except 429)
if (
error.statusCode >= 400 &&
error.statusCode < 500 &&
error.statusCode !== 429
) {
throw error;
}

// Don't retry on last attempt
if (attempt === this.maxRetries) {
throw error;
}

// Calculate delay
let delay = this.baseDelay * Math.pow(2, attempt);

// Use Retry-After header for 429 errors
if (error.statusCode === 429 && error.details.retryAfter) {
delay = error.details.retryAfter * 1000;
}

console.log(
`Request failed (attempt ${attempt + 1}/${
this.maxRetries + 1
}). Retrying in ${delay}ms...`
);
await this.sleep(delay);
}
}

throw lastError;
}

sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
}

// Usage
const retryableRequest = new RetryableRequest(3, 1000);

try {
const result = await retryableRequest.execute(() =>
makeApiRequest("/api/public/budgets")
);
console.log("Success:", result);
} catch (error) {
console.error("Failed after retries:", BillogetErrorHandler.handle(error));
}

πŸ”§ Error Recovery Strategies​

1. Circuit Breaker Pattern​

class CircuitBreaker {
constructor(threshold = 5, timeout = 60000) {
this.threshold = threshold;
this.timeout = timeout;
this.failures = 0;
this.lastFailureTime = null;
this.state = "CLOSED"; // CLOSED, OPEN, HALF_OPEN
}

async execute(requestFn) {
if (this.state === "OPEN") {
if (Date.now() - this.lastFailureTime > this.timeout) {
this.state = "HALF_OPEN";
} else {
throw new Error("Circuit breaker is OPEN");
}
}

try {
const result = await requestFn();
this.onSuccess();
return result;
} catch (error) {
this.onFailure();
throw error;
}
}

onSuccess() {
this.failures = 0;
this.state = "CLOSED";
}

onFailure() {
this.failures++;
this.lastFailureTime = Date.now();

if (this.failures >= this.threshold) {
this.state = "OPEN";
}
}
}

2. Graceful Degradation​

class BillogetAPIWithFallback {
constructor(apiKey) {
this.apiKey = apiKey;
this.cache = new Map();
}

async getBudgets(options = {}) {
try {
const budgets = await this.makeRequest("/api/public/budgets", options);
this.cache.set("budgets", budgets);
return budgets;
} catch (error) {
console.warn("API request failed, using cached data:", error.message);

const cached = this.cache.get("budgets");
if (cached) {
return {
...cached,
_fromCache: true,
_warning: "Data may be outdated due to API error",
};
}

// Return empty result with error info
return {
data: [],
pagination: { page: 1, limit: 10, total: 0 },
_error: error.message,
_fallback: true,
};
}
}
}

πŸ“Š Error Monitoring​

1. Error Tracking​

class ErrorTracker {
constructor() {
this.errors = [];
this.errorCounts = new Map();
}

track(error) {
const errorInfo = {
timestamp: new Date().toISOString(),
error: error.error,
message: error.message,
statusCode: error.statusCode,
requestId: error.requestId,
};

this.errors.push(errorInfo);

// Count error types
const key = `${error.statusCode}_${error.error}`;
this.errorCounts.set(key, (this.errorCounts.get(key) || 0) + 1);

// Alert on high error rates
if (this.shouldAlert(error)) {
this.sendAlert(errorInfo);
}
}

shouldAlert(error) {
// Alert on authentication errors
if (error.statusCode === 401 || error.statusCode === 403) {
return true;
}

// Alert on high error rates
const recentErrors = this.errors.filter(
(e) => Date.now() - new Date(e.timestamp).getTime() < 300000 // 5 minutes
);

return recentErrors.length > 10;
}

sendAlert(errorInfo) {
console.error("🚨 HIGH ERROR RATE DETECTED:", errorInfo);
// Send to monitoring service
}

getErrorStats() {
return {
totalErrors: this.errors.length,
errorCounts: Object.fromEntries(this.errorCounts),
recentErrors: this.errors.slice(-10),
};
}
}

2. Health Check​

class APIHealthChecker {
constructor(apiKey) {
this.apiKey = apiKey;
this.isHealthy = true;
this.lastCheck = null;
}

async checkHealth() {
try {
const response = await makeApiRequest("/api/public/budgets?limit=1");
this.isHealthy = true;
this.lastCheck = new Date();
return { healthy: true, timestamp: this.lastCheck };
} catch (error) {
this.isHealthy = false;
this.lastCheck = new Date();

return {
healthy: false,
timestamp: this.lastCheck,
error: error.message,
statusCode: error.statusCode,
};
}
}

async startHealthMonitoring(intervalMs = 60000) {
setInterval(async () => {
const health = await this.checkHealth();
if (!health.healthy) {
console.warn("API health check failed:", health);
}
}, intervalMs);
}
}

🎯 Best Practices​

1. Always Handle Errors​

Never ignore API errors - they provide valuable debugging information.

2. Use Appropriate Error Messages​

Provide meaningful error messages to users while logging technical details.

3. Implement Retry Logic​

Retry transient errors (5xx, 429) but not client errors (4xx).

4. Monitor Error Rates​

Track error patterns to identify issues early.

5. Provide Fallbacks​

Have graceful degradation strategies for critical functionality.

πŸ§ͺ Testing Error Scenarios​

Error Simulation​

async function testErrorHandling() {
const scenarios = [
{ name: "Invalid API Key", apiKey: "invalid_key" },
{ name: "Missing Authorization", apiKey: null },
{
name: "Insufficient Scope",
endpoint: "/api/public/budgets",
method: "POST",
},
{ name: "Not Found", endpoint: "/api/public/budgets/nonexistent" },
{ name: "Validation Error", data: { name: "", email: "invalid-email" } },
];

for (const scenario of scenarios) {
try {
console.log(`Testing: ${scenario.name}`);
await makeApiRequest(scenario.endpoint || "/api/public/budgets", {
method: scenario.method || "GET",
body: scenario.data ? JSON.stringify(scenario.data) : undefined,
});
} catch (error) {
console.log(`βœ… ${scenario.name}: ${error.message}`);
}
}
}

πŸš€ Next Steps​

Now that you understand error handling, you're ready to:

  1. Explore the API Reference - Learn about all available endpoints
  2. Set up Webhooks - Get real-time notifications
  3. Webhooks - Handle webhook errors and retries

πŸ“š Additional Resources​


Congratulations! You've completed the Getting Started guide. Ready to explore the API Reference? πŸŽ‰