Skip to main content

Overview

Robust error handling is critical for production integrations. This guide covers common error scenarios, retry strategies, and best practices for building resilient applications.

Error Response Format

All Day Copilot API errors follow a consistent format:
{
  "error": "Error Type",
  "message": "Human-readable error description",
  "status": 400,
  "correlationId": "req_abc123xyz",
  "details": {
    "field": "title",
    "constraint": "required"
  }
}
Key Fields:
  • error: Error category (e.g., “Validation Error”, “Not Found”)
  • message: Detailed explanation
  • status: HTTP status code
  • correlationId: Unique ID for support/debugging
  • details: Additional context (optional)

HTTP Status Codes

CodeMeaningTypical CauseAction
400Bad RequestInvalid parametersFix request format
401UnauthorizedMissing/invalid tokenCheck authentication
403ForbiddenInsufficient permissionsCheck user access
404Not FoundResource doesn’t existVerify ID/check deletion
409ConflictDraft conflictRefresh and retry
422Unprocessable EntityValidation failedFix validation errors
429Too Many RequestsRate limit exceededImplement backoff
500Internal Server ErrorServer issueRetry with backoff
503Service UnavailableMaintenance/outageRetry later

Common Error Scenarios

1. Authentication Errors (401)

Cause: Invalid or expired token
{
  "error": "Unauthorized",
  "message": "Missing or invalid authentication token",
  "status": 401
}
Solution:
async function makeAuthenticatedRequest(url, options) {
  const response = await fetch(url, {
    ...options,
    headers: {
      ...options.headers,
      'Authorization': `Bearer ${getToken()}`
    }
  });

  if (response.status === 401) {
    // Token expired, refresh it
    const newToken = await refreshAuthToken();

    // Retry with new token
    return fetch(url, {
      ...options,
      headers: {
        ...options.headers,
        'Authorization': `Bearer ${newToken}`
      }
    });
  }

  return response;
}

2. Validation Errors (422)

Cause: Request body doesn’t meet validation requirements
{
  "error": "Validation Error",
  "message": "Field 'title' is required",
  "status": 422,
  "details": {
    "field": "title",
    "constraint": "required",
    "value": null
  }
}
Solution:
async function createTask(data) {
  try {
    const response = await fetch('https://app.daycopilot.ai/api/v1/tasks', {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${token}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify(data)
    });

    if (response.status === 422) {
      const error = await response.json();
      throw new ValidationError(error.message, error.details);
    }

    return response.json();
  } catch (error) {
    if (error instanceof ValidationError) {
      console.error('Validation failed:', error.field, error.constraint);
      // Show user-friendly message in UI
    }
    throw error;
  }
}

class ValidationError extends Error {
  constructor(message, details) {
    super(message);
    this.name = 'ValidationError';
    this.field = details?.field;
    this.constraint = details?.constraint;
  }
}

3. Not Found Errors (404)

Cause: Resource doesn’t exist or user lacks access (RLS)
{
  "error": "Not Found",
  "message": "Task with ID abc-123 does not exist",
  "status": 404
}
Important: For security reasons, Day Copilot returns 404 (not 403) when you lack access to a resource. Solution:
async function getTask(taskId) {
  try {
    const response = await fetch(
      `https://app.daycopilot.ai/api/v1/tasks/${taskId}`,
      { headers: { 'Authorization': `Bearer ${token}` } }
    );

    if (response.status === 404) {
      // Could be: doesn't exist, deleted, or no access
      console.warn(`Task ${taskId} not found or inaccessible`);
      return null;
    }

    return response.json();
  } catch (error) {
    console.error('Failed to fetch task:', error);
    return null;
  }
}

4. Conflict Errors (409)

Cause: Draft conflict - another version was published
{
  "error": "Draft conflict",
  "message": "Another draft was published while yours was being edited",
  "status": 409,
  "data": {
    "your_version": "1.2",
    "current_version": "2.0",
    "conflicting_fields": ["title", "priority"]
  }
}
Solution:
async function publishDraftWithRetry(taskId) {
  const MAX_RETRIES = 3;

  for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
    try {
      const response = await fetch(
        `https://app.daycopilot.ai/api/v1/tasks/${taskId}/draft`,
        {
          method: 'POST',
          headers: {
            'Authorization': `Bearer ${token}`,
            'Content-Type': 'application/json'
          },
          body: JSON.stringify({ action: 'accept' })
        }
      );

      if (response.status === 409) {
        // Conflict detected
        console.log(`Conflict on attempt ${attempt + 1}, fetching latest...`);

        // Fetch current published version
        const current = await fetch(
          `https://app.daycopilot.ai/api/v1/tasks/${taskId}`,
          { headers: { 'Authorization': `Bearer ${token}` } }
        ).then(r => r.json());

        // Re-apply our changes to latest version
        await fetch(
          `https://app.daycopilot.ai/api/v1/tasks/${taskId}`,
          {
            method: 'PUT',
            headers: {
              'Authorization': `Bearer ${token}`,
              'Content-Type': 'application/json'
            },
            body: JSON.stringify(ourChanges)
          }
        );

        // Retry publish
        continue;
      }

      return response.json();
    } catch (error) {
      if (attempt === MAX_RETRIES - 1) throw error;
    }
  }

  throw new Error('Failed to publish draft after retries');
}

5. Rate Limit Errors (429)

Cause: Too many requests in time window
{
  "error": "Rate limit exceeded",
  "message": "Too many requests",
  "status": 429,
  "retryAfter": 60
}
Response Headers:
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1699564800
Retry-After: 60
Solution:
async function makeRequestWithRateLimit(url, options) {
  const response = await fetch(url, options);

  if (response.status === 429) {
    const retryAfter = response.headers.get('Retry-After') || 60;
    console.log(`Rate limited. Retrying in ${retryAfter} seconds...`);

    await new Promise(resolve => setTimeout(resolve, retryAfter * 1000));
    return makeRequestWithRateLimit(url, options);
  }

  return response;
}

6. Server Errors (500, 503)

Cause: Temporary server issues or maintenance
{
  "error": "Internal Server Error",
  "message": "An unexpected error occurred",
  "status": 500,
  "correlationId": "req_abc123xyz"
}
Solution:
async function makeRequestWithRetry(url, options, maxRetries = 3) {
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      const response = await fetch(url, options);

      // Retry on 5xx errors
      if (response.status >= 500 && response.status < 600) {
        if (attempt < maxRetries - 1) {
          const delay = Math.pow(2, attempt) * 1000; // Exponential backoff
          console.log(`Server error, retrying in ${delay}ms...`);
          await new Promise(resolve => setTimeout(resolve, delay));
          continue;
        }
      }

      return response;
    } catch (error) {
      if (attempt === maxRetries - 1) throw error;
      const delay = Math.pow(2, attempt) * 1000;
      await new Promise(resolve => setTimeout(resolve, delay));
    }
  }
}

Retry Strategies

Exponential Backoff

class APIClient {
  async requestWithBackoff(url, options, maxRetries = 5) {
    for (let attempt = 0; attempt < maxRetries; attempt++) {
      try {
        const response = await fetch(url, options);

        // Don't retry client errors (4xx except 429)
        if (response.status >= 400 && response.status < 500 && response.status !== 429) {
          throw new Error(await response.text());
        }

        // Retry server errors and rate limits
        if (response.status === 429 || response.status >= 500) {
          const delay = Math.min(1000 * Math.pow(2, attempt), 32000);
          console.log(`Retry attempt ${attempt + 1} after ${delay}ms`);
          await this.sleep(delay);
          continue;
        }

        return response;
      } catch (error) {
        if (attempt === maxRetries - 1) throw error;
      }
    }
  }

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

Circuit Breaker Pattern

class CircuitBreaker {
  constructor(failureThreshold = 5, timeout = 60000) {
    this.failureThreshold = failureThreshold;
    this.timeout = timeout;
    this.failures = 0;
    this.state = 'CLOSED';  // CLOSED, OPEN, HALF_OPEN
    this.nextAttempt = Date.now();
  }

  async execute(fn) {
    if (this.state === 'OPEN') {
      if (Date.now() < this.nextAttempt) {
        throw new Error('Circuit breaker is OPEN');
      }
      this.state = 'HALF_OPEN';
    }

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

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

  onFailure() {
    this.failures++;
    if (this.failures >= this.failureThreshold) {
      this.state = 'OPEN';
      this.nextAttempt = Date.now() + this.timeout;
      console.warn('Circuit breaker opened due to failures');
    }
  }
}

// Usage
const breaker = new CircuitBreaker(5, 60000);

async function makeAPICall() {
  return breaker.execute(async () => {
    return fetch('https://app.daycopilot.ai/api/v1/tasks', {
      headers: { 'Authorization': `Bearer ${token}` }
    });
  });
}

Logging & Debugging

Using Correlation IDs

async function makeRequest(url, options) {
  try {
    const response = await fetch(url, options);

    if (!response.ok) {
      const error = await response.json();
      console.error('API Error:', {
        url,
        status: response.status,
        correlationId: error.correlationId,
        message: error.message
      });

      // Save correlation ID for support ticket
      saveErrorLog({
        correlationId: error.correlationId,
        timestamp: new Date().toISOString(),
        endpoint: url,
        error: error.message
      });
    }

    return response;
  } catch (error) {
    console.error('Network error:', error);
    throw error;
  }
}

Error Monitoring

class ErrorMonitor {
  constructor() {
    this.errors = [];
  }

  logError(error, context) {
    const entry = {
      timestamp: new Date().toISOString(),
      error: error.message,
      correlationId: error.correlationId,
      context,
      stackTrace: error.stack
    };

    this.errors.push(entry);

    // Send to monitoring service (e.g., Sentry, Datadog)
    if (typeof window !== 'undefined' && window.Sentry) {
      window.Sentry.captureException(error, {
        tags: {
          correlationId: error.correlationId
        },
        extra: context
      });
    }
  }

  getRecentErrors(count = 10) {
    return this.errors.slice(-count);
  }
}

Best Practices

Log enough information to debug issues:
try {
  await createTask(data);
} catch (error) {
  console.error('Failed to create task:', {
    error: error.message,
    correlationId: error.correlationId,
    input: data,
    timestamp: new Date().toISOString()
  });
}
For transient errors (rate limits, server issues), use exponential backoff instead of immediate retries.
  • Retriable: 429, 500, 503, network errors
  • Non-retriable: 400, 401, 403, 404, 422
Don’t waste resources retrying validation errors.
Implement circuit breakers to prevent cascading failures when the API is experiencing issues.
Always save correlation IDs from error responses. They’re essential for support tickets and debugging.
When API calls fail, provide fallback behavior:
async function getTasks() {
  try {
    return await fetchFromAPI();
  } catch (error) {
    console.warn('API unavailable, using cache');
    return getFromLocalCache();
  }
}

Production-Ready Error Handling

Complete example with all best practices:
class DayCopilotClient {
  constructor(token) {
    this.token = token;
    this.baseURL = 'https://app.daycopilot.ai/api/v1';
    this.breaker = new CircuitBreaker(5, 60000);
  }

  async request(endpoint, options = {}) {
    const url = `${this.baseURL}${endpoint}`;
    const maxRetries = options.retriable !== false ? 3 : 1;

    return this.breaker.execute(async () => {
      for (let attempt = 0; attempt < maxRetries; attempt++) {
        try {
          const response = await fetch(url, {
            ...options,
            headers: {
              'Authorization': `Bearer ${this.token}`,
              'Content-Type': 'application/json',
              ...options.headers
            }
          });

          // Handle specific status codes
          if (response.status === 401) {
            await this.refreshToken();
            continue; // Retry with new token
          }

          if (response.status === 429) {
            const retryAfter = response.headers.get('Retry-After') || 60;
            await this.sleep(retryAfter * 1000);
            continue;
          }

          if (response.status >= 500) {
            if (attempt < maxRetries - 1) {
              const delay = Math.pow(2, attempt) * 1000;
              await this.sleep(delay);
              continue;
            }
          }

          if (!response.ok) {
            const error = await response.json();
            throw new APIError(error);
          }

          return response.json();
        } catch (error) {
          if (attempt === maxRetries - 1) {
            this.logError(error, { endpoint, attempt });
            throw error;
          }
        }
      }
    });
  }

  async refreshToken() {
    // Implement token refresh logic
    console.log('Refreshing authentication token...');
  }

  logError(error, context) {
    console.error('API Error:', {
      message: error.message,
      correlationId: error.correlationId,
      context
    });
  }

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

class APIError extends Error {
  constructor(errorResponse) {
    super(errorResponse.message);
    this.name = 'APIError';
    this.status = errorResponse.status;
    this.correlationId = errorResponse.correlationId;
    this.details = errorResponse.details;
  }
}

Next Steps