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β
Field | Type | Description |
---|---|---|
error | string | Machine-readable error identifier |
message | string | Human-readable error description |
details | object | Additional error context (optional) |
timestamp | string | ISO 8601 timestamp of the error |
requestId | string | Unique request identifier for debugging |
π HTTP Status Codesβ
2xx Successβ
Code | Status | Description |
---|---|---|
200 | OK | Request successful |
201 | Created | Resource created successfully |
204 | No Content | Request successful, no content returned |
4xx Client Errorsβ
Code | Status | Description |
---|---|---|
400 | Bad Request | Invalid request format or parameters |
401 | Unauthorized | Missing or invalid authentication |
403 | Forbidden | Insufficient permissions |
404 | Not Found | Resource not found |
409 | Conflict | Resource conflict (duplicate, etc.) |
422 | Unprocessable Entity | Validation errors |
429 | Too Many Requests | Rate limit exceeded |
5xx Server Errorsβ
Code | Status | Description |
---|---|---|
500 | Internal Server Error | Unexpected server error |
502 | Bad Gateway | Upstream service error |
503 | Service Unavailable | Service temporarily unavailable |
504 | Gateway Timeout | Request 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:
- Explore the API Reference - Learn about all available endpoints
- Set up Webhooks - Get real-time notifications
- Webhooks - Handle webhook errors and retries
π Additional Resourcesβ
- API Reference - Complete endpoint documentation
- Rate Limiting - Understand and handle rate limits
- Postman Collection - Test error scenarios
- Webhooks - Error handling for webhook deliveries
Congratulations! You've completed the Getting Started guide. Ready to explore the API Reference? π