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 messageerror.details- Optional additional contexttime- 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
- @cloudillo/base - FetchError class
- REST API - API endpoint documentation
- Authentication - Auth error handling