Skip to main content

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? ๐ŸŽ‰