Error Handling

Error Handling

Cloudillo uses structured error codes and standardized error responses for consistent error handling across the platform.

Error Response Format

All API errors return this structure:

{
  "error": {
    "code": "E-AUTH-UNAUTH",
    "message": "Unauthorized access",
    "details": {
      "reason": "Token expired",
      "expiredAt": 1735000000
    }
  },
  "time": 1735000000,
  "reqId": "req_abc123"
}

Fields:

  • error.code - Structured error code (see below)
  • error.message - Human-readable error message
  • error.details - Optional additional context
  • time - Unix timestamp (seconds)
  • reqId - Request ID for tracing

Error Code Format

Error codes follow the pattern: E-MODULE-ERRTYPE

  • E- - Prefix (all error codes start with this)
  • MODULE - Module identifier (AUTH, CORE, SYS, etc.)
  • ERRTYPE - Error type (UNAUTH, NOTFOUND, etc.)

Error Codes

Authentication Errors (AUTH)

Code HTTP Status Description
E-AUTH-UNAUTH 401 Unauthorized - Invalid or missing token
E-AUTH-FORBID 403 Forbidden - Insufficient permissions
E-AUTH-EXPIRED 401 Token expired
E-AUTH-INVALID 400 Invalid credentials
E-AUTH-MISMATCH 400 Password mismatch
E-AUTH-EXISTS 409 User already exists

Core Errors (CORE)

Code HTTP Status Description
E-CORE-NOTFOUND 404 Resource not found
E-CORE-CONFLICT 409 Resource conflict (duplicate)
E-CORE-INVALID 400 Invalid request data
E-CORE-BADREQ 400 Malformed request
E-CORE-LIMIT 429 Rate limit exceeded

System Errors (SYS)

Code HTTP Status Description
E-SYS-UNAVAIL 503 Service unavailable
E-SYS-INTERNAL 500 Internal server error
E-SYS-TIMEOUT 504 Request timeout
E-SYS-STORAGE 507 Storage full

File Errors (FILE)

Code HTTP Status Description
E-FILE-NOTFOUND 404 File not found
E-FILE-TOOLARGE 413 File too large
E-FILE-BADTYPE 415 Unsupported file type
E-FILE-CORRUPT 422 Corrupted file

Action Errors (ACTION)

Code HTTP Status Description
E-ACTION-NOTFOUND 404 Action not found
E-ACTION-INVALID 400 Invalid action data
E-ACTION-DENIED 403 Action not allowed
E-ACTION-EXPIRED 410 Action expired

Handling Errors with FetchError

The @cloudillo/base library provides FetchError for consistent error handling:

import { FetchError } from '@cloudillo/base'

try {
  const api = cloudillo.createApiClient()
  const profile = await api.me.get()
} catch (error) {
  if (error instanceof FetchError) {
    console.error('Error code:', error.code)
    console.error('Message:', error.message)
    console.error('HTTP status:', error.status)
    console.error('Details:', error.details)

    // Handle specific errors
    switch (error.code) {
      case 'E-AUTH-UNAUTH':
        // Redirect to login
        window.location.href = '/login'
        break

      case 'E-AUTH-FORBID':
        // Show permission error
        alert('You do not have permission to access this resource')
        break

      case 'E-CORE-NOTFOUND':
        // Show not found message
        console.log('Resource not found')
        break

      case 'E-CORE-LIMIT':
        // Rate limited
        console.log('Too many requests, please slow down')
        break

      default:
        // Generic error
        console.error('An error occurred:', error.message)
    }
  } else {
    // Non-API error (network, parsing, etc.)
    console.error('Unexpected error:', error)
  }
}

Error Handling Patterns

Pattern 1: Global Error Handler

function createApiWithErrorHandling() {
  const api = cloudillo.createApiClient()

  // Wrap API methods with error handling
  return new Proxy(api, {
    get(target, prop) {
      const original = target[prop]

      if (typeof original === 'function') {
        return async (...args) => {
          try {
            return await original.apply(target, args)
          } catch (error) {
            handleApiError(error)
            throw error
          }
        }
      }

      return original
    }
  })
}

function handleApiError(error) {
  if (error instanceof FetchError) {
    switch (error.code) {
      case 'E-AUTH-UNAUTH':
      case 'E-AUTH-EXPIRED':
        // Redirect to login
        window.location.href = '/login'
        break

      case 'E-CORE-LIMIT':
        // Show rate limit toast
        showToast('Too many requests. Please slow down.')
        break

      default:
        // Log to error tracking service
        logErrorToSentry(error)
    }
  }
}

Pattern 2: React Error Boundary

import { Component } from 'react'
import { FetchError } from '@cloudillo/base'

class ApiErrorBoundary extends Component {
  state = { error: null }

  static getDerivedStateFromError(error) {
    return { error }
  }

  componentDidCatch(error, errorInfo) {
    if (error instanceof FetchError) {
      console.error('API Error:', error.code, error.message)

      // Handle specific errors
      if (error.code === 'E-AUTH-UNAUTH') {
        window.location.href = '/login'
      }
    }
  }

  render() {
    if (this.state.error) {
      return (
        <div className="error">
          <h1>Something went wrong</h1>
          <p>{this.state.error.message}</p>
          <button onClick={() => this.setState({ error: null })}>
            Try again
          </button>
        </div>
      )
    }

    return this.props.children
  }
}

Pattern 3: Retry Logic

async function fetchWithRetry(fn, maxRetries = 3) {
  for (let i = 0; i < maxRetries; i++) {
    try {
      return await fn()
    } catch (error) {
      if (error instanceof FetchError) {
        // Retry on transient errors
        if (error.code === 'E-SYS-UNAVAIL' ||
            error.code === 'E-SYS-TIMEOUT') {
          if (i < maxRetries - 1) {
            // Exponential backoff
            await new Promise(r => setTimeout(r, 1000 * Math.pow(2, i)))
            continue
          }
        }

        // Don't retry on auth or client errors
        if (error.code.startsWith('E-AUTH-') ||
            error.code.startsWith('E-CORE-')) {
          throw error
        }
      }

      throw error
    }
  }
}

// Usage
const data = await fetchWithRetry(() => api.me.get())

Pattern 4: User-Friendly Messages

function getUserFriendlyMessage(error: FetchError): string {
  const messages: Record<string, string> = {
    'E-AUTH-UNAUTH': 'Please log in to continue',
    'E-AUTH-FORBID': 'You don\'t have permission to do that',
    'E-AUTH-EXPIRED': 'Your session has expired. Please log in again',
    'E-CORE-NOTFOUND': 'The item you\'re looking for doesn\'t exist',
    'E-CORE-LIMIT': 'You\'re making too many requests. Please slow down',
    'E-FILE-TOOLARGE': 'This file is too large. Maximum size is 100MB',
    'E-FILE-BADTYPE': 'This file type is not supported',
    'E-SYS-UNAVAIL': 'The service is temporarily unavailable. Please try again later',
  }

  return messages[error.code] || 'An unexpected error occurred. Please try again'
}

// Usage
try {
  await api.file.upload.post(formData)
} catch (error) {
  if (error instanceof FetchError) {
    alert(getUserFriendlyMessage(error))
  }
}

Pattern 5: Typed Error Handling

type ApiError = {
  code: string
  message: string
  status: number
}

function isAuthError(error: ApiError): boolean {
  return error.code.startsWith('E-AUTH-')
}

function isClientError(error: ApiError): boolean {
  return error.status >= 400 && error.status < 500
}

function isServerError(error: ApiError): boolean {
  return error.status >= 500
}

// Usage
try {
  const data = await api.action.post(newAction)
} catch (error) {
  if (error instanceof FetchError) {
    if (isAuthError(error)) {
      handleAuthError(error)
    } else if (isServerError(error)) {
      showRetryPrompt()
    }
  }
}

Validation Errors

Client-side validation can prevent many errors:

import type { NewAction } from '@cloudillo/types'

function validateAction(action: NewAction): string[] {
  const errors: string[] = []

  if (!action.type) {
    errors.push('Action type is required')
  }

  if (action.type === 'POST' && !action.content) {
    errors.push('Post content is required')
  }

  if (action.attachments && action.attachments.length > 10) {
    errors.push('Maximum 10 attachments allowed')
  }

  return errors
}

// Usage
const newAction = {
  type: 'POST',
  content: { text: 'Hello!' }
}

const validationErrors = validateAction(newAction)
if (validationErrors.length > 0) {
  alert('Validation errors:\n' + validationErrors.join('\n'))
  return
}

// Proceed with API call
await api.action.post(newAction)

Logging and Monitoring

function logApiError(error: FetchError, context?: any) {
  const logData = {
    code: error.code,
    message: error.message,
    status: error.status,
    url: error.response?.url,
    context,
    timestamp: new Date().toISOString(),
    userAgent: navigator.userAgent,
    user: cloudillo.idTag
  }

  // Send to logging service
  fetch('/api/logs/error', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(logData)
  })

  // Also log to console in development
  if (process.env.NODE_ENV === 'development') {
    console.error('API Error:', logData)
  }
}

Best Practices

1. Always Handle Errors

// ✅ Good - handle errors
try {
  await api.action.post(newAction)
} catch (error) {
  handleError(error)
}

// ❌ Bad - unhandled errors crash the app
await api.action.post(newAction)

2. Provide User Feedback

// ✅ Good - user knows what happened
try {
  await api.file.upload.post(formData)
  showToast('File uploaded successfully!')
} catch (error) {
  showToast('Upload failed. Please try again.')
}

// ❌ Bad - silent failure
try {
  await api.file.upload.post(formData)
} catch (error) {
  console.error(error)
}

3. Differentiate Error Types

// ✅ Good - handle different errors appropriately
if (error.code === 'E-AUTH-UNAUTH') {
  redirectToLogin()
} else if (error.code === 'E-CORE-NOTFOUND') {
  show404Page()
} else {
  showGenericError()
}

// ❌ Bad - same handling for all errors
alert('Error!')

4. Log Errors for Debugging

// ✅ Good - logs help debug issues
catch (error) {
  console.error('Failed to create post:', {
    error,
    action: newAction,
    user: cloudillo.idTag
  })
  showError('Failed to create post')
}

// ❌ Bad - no debugging info
catch (error) {
  showError('Failed')
}

See Also