Overview
Proper error handling is essential for building reliable applications with the Babou API. This guide covers common error scenarios and best practices for handling them gracefully.Error Response Structure
All API errors follow a consistent format:Copy
{
"error": "Human-readable error message",
"code": "ERROR_CODE",
"hint": "Optional guidance on fixing the error"
}
Always check the
code field for programmatic error handling, not the error message which may change.Basic Error Handling
Check Response Status
Always verify the response status before processing:Copy
async function apiCall(url: string, options: RequestInit) {
const response = await fetch(url, options);
if (!response.ok) {
const error = await response.json();
throw new ApiError(
error.error,
error.code,
response.status,
error.hint
);
}
return await response.json();
}
class ApiError extends Error {
constructor(
message: string,
public code: string,
public status: number,
public hint?: string
) {
super(message);
this.name = 'ApiError';
}
}
// Usage
try {
const project = await apiCall('https://api.babou.ai/api/v1/projects', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.BABOU_API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ name: 'Test Project' })
});
console.log('✓ Created:', project.id);
} catch (error) {
if (error instanceof ApiError) {
console.error(`Error ${error.status}: ${error.message}`);
console.error(`Code: ${error.code}`);
if (error.hint) console.error(`Hint: ${error.hint}`);
}
}
Handling Specific Errors
Authentication Errors
UNAUTHORIZED (401)
UNAUTHORIZED (401)
Cause: Invalid or missing API keyStrategy:Prevention:
Copy
try {
return await apiCall(url, options);
} catch (error) {
if (error instanceof ApiError && error.code === 'UNAUTHORIZED') {
console.error('Authentication failed');
console.error('Check your API key at: https://babou.ai/settings');
// Don't retry - auth errors won't resolve automatically
throw new Error('Invalid API key - please check your credentials');
}
throw error;
}
- Validate API key format before making requests
- Check API key hasn’t expired
- Use environment variables, never hardcode keys
API_KEY_EXPIRED (401)
API_KEY_EXPIRED (401)
Cause: API key has passed expiration dateStrategy:Prevention:
Copy
if (error.code === 'API_KEY_EXPIRED') {
console.error('API key has expired');
console.error('Generate a new key at: https://babou.ai/settings');
// Notify user/admin to update key
await notifyAdmin('API key expired - action required');
throw new Error('API key expired - cannot proceed');
}
- Set up key rotation schedule
- Monitor key expiration dates
- Use multiple keys for different environments
Validation Errors
VALIDATION_ERROR (400)
VALIDATION_ERROR (400)
Cause: Request parameters don’t meet requirementsStrategy:Prevention:
Copy
if (error.code === 'VALIDATION_ERROR') {
console.error('Validation failed:', error.message);
if (error.hint) {
console.error('Hint:', error.hint);
}
// Log details for debugging
logValidationError({
endpoint: url,
data: requestBody,
error: error.message,
hint: error.hint
});
// Don't retry - fix validation issues first
throw new Error(`Validation failed: ${error.hint || error.message}`);
}
Copy
function validateProject(name: string, description?: string) {
const errors = [];
if (!name || name.length < 1 || name.length > 30) {
errors.push('Name must be 1-30 characters');
}
if (description && description.length > 1000) {
errors.push('Description must be max 1000 characters');
}
if (errors.length > 0) {
throw new Error(`Validation errors:\n${errors.join('\n')}`);
}
return true;
}
// Validate before API call
validateProject(name, description);
await createProject(name, description);
Resource Errors
NOT_FOUND (404)
NOT_FOUND (404)
Cause: Resource doesn’t exist or you don’t have accessStrategy:Prevention:
Copy
if (error.code === 'NOT_FOUND') {
console.warn(`Resource not found: ${resourceId}`);
// Try to find the resource
const resources = await listResources();
const exists = resources.find(r => r.id === resourceId);
if (!exists) {
throw new Error(`Resource ${resourceId} does not exist`);
} else {
throw new Error(`No access to resource ${resourceId}`);
}
}
Copy
async function safeGetProject(projectId: string) {
try {
return await getProject(projectId);
} catch (error) {
if (error.code === 'NOT_FOUND') {
// Fallback: list and find
const { projects } = await listProjects();
const project = projects.find(p => p.id === projectId);
if (project) return project;
// Suggest alternatives
console.log('Did you mean one of these?');
projects.slice(0, 5).forEach(p => {
console.log(`- ${p.name} (${p.id})`);
});
}
throw error;
}
}
CONFLICT (409)
CONFLICT (409)
Cause: Resource state conflictStrategy:Prevention:
Copy
if (error.code === 'CONFLICT') {
console.warn('Conflict:', error.message);
// Check if we should wait and retry
if (error.message.includes('already processing')) {
console.log('Waiting for current operation to complete...');
await new Promise(r => setTimeout(r, 10000)); // Wait 10s
// Retry once
try {
return await apiCall(url, options);
} catch (retryError) {
// If still failing, give up
throw new Error(`Still conflicting after retry: ${error.message}`);
}
}
throw error;
}
Copy
async function submitPromptSafe(
projectId: string,
chapterId: string,
content: string
) {
// Check current status first
const chapter = await getChapter(projectId, chapterId);
const latestPrompt = chapter.prompts?.[chapter.prompts.length - 1];
if (latestPrompt && latestPrompt.status === 'processing') {
console.log('Prompt already processing, waiting...');
// Wait for completion
await waitForPromptCompletion(projectId, chapterId);
}
// Now safe to submit
return await submitPrompt(projectId, chapterId, content);
}
Rate Limiting
RATE_LIMIT_EXCEEDED (429)
RATE_LIMIT_EXCEEDED (429)
Cause: Too many requests in short periodStrategy:Prevention:
Copy
async function apiCallWithRetry(url: string, options: RequestInit, maxRetries = 3) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await apiCall(url, options);
} catch (error) {
if (error.code === 'RATE_LIMIT_EXCEEDED') {
if (attempt === maxRetries - 1) {
throw new Error('Rate limit exceeded after retries');
}
// Exponential backoff
const delay = Math.pow(2, attempt) * 1000;
console.log(`Rate limited, retrying in ${delay}ms...`);
await new Promise(r => setTimeout(r, delay));
continue;
}
throw error;
}
}
}
Copy
class RateLimiter {
private queue: Array<() => Promise<any>> = [];
private processing = false;
private requestsPerSecond = 10;
async add<T>(fn: () => Promise<T>): Promise<T> {
return new Promise((resolve, reject) => {
this.queue.push(async () => {
try {
const result = await fn();
resolve(result);
} catch (error) {
reject(error);
}
});
this.process();
});
}
private async process() {
if (this.processing || this.queue.length === 0) return;
this.processing = true;
while (this.queue.length > 0) {
const fn = this.queue.shift()!;
await fn();
await new Promise(r => setTimeout(r, 1000 / this.requestsPerSecond));
}
this.processing = false;
}
}
// Usage
const limiter = new RateLimiter();
for (const item of items) {
await limiter.add(() => processItem(item));
}
Retry Strategies
Exponential Backoff
Copy
async function retryWithBackoff<T>(
fn: () => Promise<T>,
options = {
maxRetries: 3,
baseDelay: 1000,
maxDelay: 30000
}
): Promise<T> {
for (let attempt = 0; attempt < options.maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
const isLastAttempt = attempt === options.maxRetries - 1;
// Don't retry on client errors (except rate limiting)
if (error instanceof ApiError) {
if (error.status >= 400 && error.status < 500 && error.status !== 429) {
throw error; // Client errors won't resolve with retry
}
}
if (isLastAttempt) {
throw error;
}
// Calculate delay with exponential backoff
const delay = Math.min(
options.baseDelay * Math.pow(2, attempt),
options.maxDelay
);
console.log(`Retry ${attempt + 1}/${options.maxRetries} after ${delay}ms`);
await new Promise(r => setTimeout(r, delay));
}
}
throw new Error('Max retries exceeded');
}
// Usage
const project = await retryWithBackoff(() =>
createProject('My Project', 'Description')
);
Conditional Retry
Only retry errors that might resolve:Copy
function shouldRetry(error: ApiError): boolean {
// Don't retry client errors
if (error.status >= 400 && error.status < 500) {
// Except rate limiting
return error.status === 429;
}
// Retry server errors
if (error.status >= 500) {
return true;
}
// Retry specific codes
const retryableCodes = ['INTERNAL_ERROR', 'UPLOAD_FAILED'];
return retryableCodes.includes(error.code);
}
async function retryIfNeeded<T>(fn: () => Promise<T>): Promise<T> {
try {
return await fn();
} catch (error) {
if (error instanceof ApiError && shouldRetry(error)) {
console.log('Retrying after error:', error.message);
return await retryWithBackoff(fn);
}
throw error;
}
}
Error Logging
Structured Logging
Copy
interface ErrorLog {
timestamp: string;
endpoint: string;
method: string;
status: number;
code: string;
message: string;
hint?: string;
requestData?: any;
}
function logError(error: ApiError, context: {
endpoint: string;
method: string;
requestData?: any;
}) {
const log: ErrorLog = {
timestamp: new Date().toISOString(),
endpoint: context.endpoint,
method: context.method,
status: error.status,
code: error.code,
message: error.message,
hint: error.hint,
requestData: context.requestData
};
console.error(JSON.stringify(log));
// Send to logging service
// await sendToLogService(log);
}
Error Monitoring
Copy
class ErrorMonitor {
private errorCounts: Map<string, number> = new Map();
private threshold = 5;
track(error: ApiError) {
const key = `${error.code}_${error.status}`;
const count = (this.errorCounts.get(key) || 0) + 1;
this.errorCounts.set(key, count);
if (count >= this.threshold) {
this.alert(error, count);
}
}
private alert(error: ApiError, count: number) {
console.error(`⚠️ Alert: ${error.code} occurred ${count} times`);
// Send alert to monitoring service
// await sendAlert({ error, count });
}
reset() {
this.errorCounts.clear();
}
}
const monitor = new ErrorMonitor();
try {
await apiCall(url, options);
} catch (error) {
if (error instanceof ApiError) {
monitor.track(error);
}
throw error;
}
User-Friendly Error Messages
Convert technical errors to user-friendly messages:Copy
function getUserMessage(error: ApiError): string {
const messages: Record<string, string> = {
'UNAUTHORIZED': 'Your session has expired. Please log in again.',
'API_KEY_EXPIRED': 'Your API access has expired. Please contact support.',
'VALIDATION_ERROR': `Please check your input: ${error.hint || error.message}`,
'NOT_FOUND': 'The requested item could not be found.',
'CONFLICT': 'This operation is already in progress. Please wait.',
'FILE_TOO_LARGE': 'The file is too large. Maximum size is 100MB.',
'RATE_LIMIT_EXCEEDED': 'Too many requests. Please try again in a moment.',
'INTERNAL_ERROR': 'Something went wrong. Please try again later.'
};
return messages[error.code] || 'An unexpected error occurred.';
}
// Usage in UI
try {
await uploadAsset(file);
showSuccess('File uploaded successfully!');
} catch (error) {
if (error instanceof ApiError) {
showError(getUserMessage(error));
} else {
showError('An unexpected error occurred.');
}
}
Complete Example
Putting it all together:Copy
class BabouClient {
private baseUrl = 'https://api.babou.ai/api/v1';
private apiKey: string;
private rateLimiter = new RateLimiter();
private errorMonitor = new ErrorMonitor();
constructor(apiKey: string) {
this.apiKey = apiKey;
}
private async request<T>(
endpoint: string,
options: RequestInit = {}
): Promise<T> {
const url = `${this.baseUrl}${endpoint}`;
return this.rateLimiter.add(() =>
retryWithBackoff(async () => {
try {
const response = await fetch(url, {
...options,
headers: {
'Authorization': `Bearer ${this.apiKey}`,
'Content-Type': 'application/json',
...options.headers
}
});
if (!response.ok) {
const error = await response.json();
const apiError = new ApiError(
error.error,
error.code,
response.status,
error.hint
);
this.errorMonitor.track(apiError);
logError(apiError, {
endpoint,
method: options.method || 'GET',
requestData: options.body
});
throw apiError;
}
return await response.json();
} catch (error) {
if (error instanceof ApiError) {
throw error;
}
throw new Error(`Network error: ${error.message}`);
}
})
);
}
async createProject(name: string, description?: string) {
// Validate first
validateProject(name, description);
return this.request('/projects', {
method: 'POST',
body: JSON.stringify({ name, description })
});
}
// ... other methods
}
// Usage
const client = new BabouClient(process.env.BABOU_API_KEY!);
try {
const project = await client.createProject('My Video');
console.log('✓ Success:', project.id);
} catch (error) {
console.error('✗ Failed:', getUserMessage(error));
}