Skip to main content

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:
{
  "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:
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

Cause: Invalid or missing API keyStrategy:
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;
}
Prevention:
  • Validate API key format before making requests
  • Check API key hasn’t expired
  • Use environment variables, never hardcode keys
Cause: API key has passed expiration dateStrategy:
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');
}
Prevention:
  • Set up key rotation schedule
  • Monitor key expiration dates
  • Use multiple keys for different environments

Validation Errors

Cause: Request parameters don’t meet requirementsStrategy:
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}`);
}
Prevention:
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

Cause: Resource doesn’t exist or you don’t have accessStrategy:
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}`);
  }
}
Prevention:
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;
  }
}
Cause: Resource state conflictStrategy:
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;
}
Prevention:
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

Cause: Too many requests in short periodStrategy:
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;
    }
  }
}
Prevention:
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

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:
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

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

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:
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:
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));
}

Next Steps