ObjectStackObjectStack

Error Handling

Global error codes, response formats, and debugging strategies for ObjectStack APIs

Error Handling

The Error Handling Protocol defines standardized error codes, response formats, and debugging strategies across all ObjectStack APIs (HTTP, WebSocket, GraphQL).

Why Standardized Errors Matter

Problem: Traditional APIs return errors inconsistently:

// Different error structures across endpoints 😱
API 1: { error: "Not found" }
API 2: { errors: [{ message: "Not found" }] }
API 3: { success: false, msg: "Not found", code: 404 }
API 4: HTTP 200 OK with { status: "error", ... }

Developer pain:

  • Every endpoint requires custom error handling logic
  • Difficult to show user-friendly messages
  • Debugging is nightmare (where did this error originate?)
  • Monitoring/alerting is inconsistent

Solution: ObjectStack enforces a single error format across all communication channels. Every error has a machine-readable code, human-readable message, and context for debugging.

Business Value Delivered

Better User Experience

Consistent error messages guide users to resolution. No generic 'Something went wrong' nonsense.

Faster Debugging

Error codes + request IDs = find root cause in seconds. Save hours of log hunting.

Automated Monitoring

Alert on specific error codes (e.g., RATE_LIMITED spikes = upgrade prompts working).

Security Hardening

Never leak sensitive info in errors. Attackers can't probe your system via error messages.

Standard Error Response

Every error follows this structure:

{
  "success": false,
  "error": {
    "code": "ERROR_CODE",
    "message": "Human-readable description",
    "details": { /* Additional context */ },
    "request_id": "req_abc123",
    "timestamp": "2024-01-16T14:30:00Z"
  }
}

Fields:

  • success: Always false for errors
  • error.code: Machine-readable error code (use for conditionals)
  • error.message: Human-readable message (show to users or developers)
  • error.details: Additional context (field names, constraints, etc.)
  • error.request_id: Unique request identifier for debugging
  • error.timestamp: When error occurred (ISO 8601)

HTTP Status Codes

ObjectStack uses standard HTTP status codes:

StatusMeaningWhen Used
400Bad RequestInvalid input, validation failure
401UnauthorizedAuthentication required or failed
403ForbiddenAuthenticated but insufficient permissions
404Not FoundResource doesn't exist
409ConflictResource already exists or version mismatch
422Unprocessable EntityBusiness logic validation failed
429Too Many RequestsRate limit exceeded
500Internal Server ErrorServer-side error
503Service UnavailableServer overloaded or maintenance

Important: Even on error, response body always includes JSON error object.

Error Codes

Authentication & Authorization

UNAUTHORIZED

HTTP Status: 401
Meaning: No authentication credentials provided or invalid credentials

Example:

{
  "success": false,
  "error": {
    "code": "UNAUTHORIZED",
    "message": "Authentication required",
    "details": {
      "hint": "Include 'Authorization: Bearer <token>' header"
    }
  }
}

How to fix:

  • Include valid JWT token in Authorization header
  • Refresh expired tokens
  • Re-authenticate user

INVALID_TOKEN

HTTP Status: 401
Meaning: Token is malformed or invalid

Example:

{
  "success": false,
  "error": {
    "code": "INVALID_TOKEN",
    "message": "JWT token is invalid",
    "details": {
      "reason": "signature_verification_failed"
    }
  }
}

How to fix:

  • Check token hasn't been tampered with
  • Verify token is meant for this API (check aud claim)
  • Ensure server secret key is correct

TOKEN_EXPIRED

HTTP Status: 401
Meaning: JWT token has expired

Example:

{
  "success": false,
  "error": {
    "code": "TOKEN_EXPIRED",
    "message": "JWT token expired",
    "details": {
      "expired_at": "2024-01-16T10:00:00Z",
      "current_time": "2024-01-16T14:30:00Z"
    }
  }
}

How to fix:

  • Refresh token using refresh token flow
  • Re-authenticate user
  • Check token lifetime settings (typically 15-60 minutes)

FORBIDDEN

HTTP Status: 403
Meaning: Authenticated but insufficient permissions

Example:

{
  "success": false,
  "error": {
    "code": "FORBIDDEN",
    "message": "Insufficient permissions to access this resource",
    "details": {
      "required_permission": "account:write",
      "user_permissions": ["account:read"]
    }
  }
}

How to fix:

  • Request permission from administrator
  • Check row-level security rules
  • Verify user role assignments

Validation Errors

VALIDATION_ERROR

HTTP Status: 400
Meaning: Input validation failed (schema validation)

Example:

{
  "success": false,
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Validation failed for 2 fields",
    "details": {
      "fields": [
        {
          "field": "email",
          "message": "Invalid email format",
          "constraint": "format",
          "value": "not-an-email"
        },
        {
          "field": "age",
          "message": "Must be at least 18",
          "constraint": "min",
          "value": 15,
          "expected": 18
        }
      ]
    }
  }
}

How to fix:

  • Check field constraints in object schema (GET /api/v1/meta/object/{object})
  • Validate input client-side before submission
  • Show field-specific errors in UI

Client-side handling:

if (error.code === 'VALIDATION_ERROR') {
  error.details.fields.forEach(({ field, message }) => {
    showFieldError(field, message);
  });
}

REQUIRED_FIELD

HTTP Status: 400
Meaning: Required field is missing

Example:

{
  "success": false,
  "error": {
    "code": "REQUIRED_FIELD",
    "message": "Missing required field: name",
    "details": {
      "field": "name",
      "constraint": "required"
    }
  }
}

INVALID_TYPE

HTTP Status: 400
Meaning: Field value has wrong type

Example:

{
  "success": false,
  "error": {
    "code": "INVALID_TYPE",
    "message": "Field 'age' must be a number",
    "details": {
      "field": "age",
      "expected_type": "number",
      "actual_type": "string",
      "value": "twenty-five"
    }
  }
}

Resource Errors

NOT_FOUND

HTTP Status: 404
Meaning: Requested resource doesn't exist

Example:

{
  "success": false,
  "error": {
    "code": "NOT_FOUND",
    "message": "Account with id 'acc_999' not found",
    "details": {
      "resource": "account",
      "resource_id": "acc_999"
    }
  }
}

How to fix:

  • Verify resource ID is correct
  • Check user has permission to see resource (row-level security)
  • Resource may have been deleted

ALREADY_EXISTS

HTTP Status: 409
Meaning: Resource with unique constraint already exists

Example:

{
  "success": false,
  "error": {
    "code": "ALREADY_EXISTS",
    "message": "Account with email 'john@acme.com' already exists",
    "details": {
      "resource": "account",
      "constraint": "unique",
      "field": "email",
      "value": "john@acme.com"
    }
  }
}

How to fix:

  • Check for existing resource before creating
  • Update existing resource instead of creating new one
  • Use different value for unique field

CONSTRAINT_VIOLATION

HTTP Status: 409
Meaning: Operation violates database constraint

Example:

{
  "success": false,
  "error": {
    "code": "CONSTRAINT_VIOLATION",
    "message": "Cannot delete account with active opportunities",
    "details": {
      "resource": "account",
      "resource_id": "acc_123",
      "constraint": "foreign_key",
      "related_object": "opportunity",
      "related_count": 5
    }
  }
}

How to fix:

  • Delete related records first
  • Enable cascade delete on object schema
  • Archive instead of delete (soft delete)

Rate Limiting

RATE_LIMITED

HTTP Status: 429
Meaning: Too many requests, rate limit exceeded

Example:

{
  "success": false,
  "error": {
    "code": "RATE_LIMITED",
    "message": "Rate limit exceeded",
    "details": {
      "limit": 1000,
      "window": "1m",
      "retry_after": 45,
      "quota_reset": "2024-01-16T14:31:00Z"
    }
  }
}

HTTP Headers:

HTTP/1.1 429 Too Many Requests
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1705412460
Retry-After: 45

How to fix:

  • Implement exponential backoff
  • Batch requests to reduce call count
  • Upgrade to higher tier for increased limits
  • Cache responses to avoid repeated calls

Client-side handling:

async function fetchWithRetry(url, options = {}, maxRetries = 3) {
  for (let i = 0; i < maxRetries; i++) {
    const response = await fetch(url, options);
    
    if (response.status !== 429) {
      return response;
    }
    
    const retryAfter = response.headers.get('Retry-After');
    await sleep(retryAfter * 1000);
  }
  
  throw new Error('Max retries exceeded');
}

QUOTA_EXCEEDED

HTTP Status: 429
Meaning: Monthly/daily quota exceeded

Example:

{
  "success": false,
  "error": {
    "code": "QUOTA_EXCEEDED",
    "message": "Monthly API quota exceeded",
    "details": {
      "quota": 10000,
      "used": 10000,
      "period": "monthly",
      "reset": "2024-02-01T00:00:00Z",
      "upgrade_url": "https://app.acme.com/billing/upgrade"
    }
  }
}

How to fix:

  • Wait for quota reset
  • Upgrade to higher plan
  • Optimize API usage

Business Logic Errors

BUSINESS_RULE_VIOLATION

HTTP Status: 422
Meaning: Operation violates business logic rule

Example:

{
  "success": false,
  "error": {
    "code": "BUSINESS_RULE_VIOLATION",
    "message": "Cannot close opportunity without selecting a stage reason",
    "details": {
      "rule": "opportunity_close_validation",
      "field": "stage_reason",
      "required_when": "stage === 'Closed Won' || stage === 'Closed Lost'"
    }
  }
}

How to fix:

  • Check validation rules in object schema
  • Provide required fields
  • Follow business process workflow

WORKFLOW_ERROR

HTTP Status: 422
Meaning: Workflow/automation failed

Example:

{
  "success": false,
  "error": {
    "code": "WORKFLOW_ERROR",
    "message": "Approval workflow rejected the request",
    "details": {
      "workflow": "purchase_order_approval",
      "step": "manager_approval",
      "reason": "Amount exceeds manager approval limit",
      "limit": 10000,
      "requested": 15000
    }
  }
}

Server Errors

SERVER_ERROR

HTTP Status: 500
Meaning: Internal server error

Example:

{
  "success": false,
  "error": {
    "code": "SERVER_ERROR",
    "message": "An internal error occurred",
    "details": {
      "request_id": "req_abc123",
      "support_url": "https://support.acme.com/request/req_abc123"
    }
  }
}

Important: Never leak stack traces or sensitive internals to clients.

How to fix:

  • Retry request (may be transient)
  • Check server status page
  • Contact support with request_id

SERVICE_UNAVAILABLE

HTTP Status: 503
Meaning: Server temporarily unavailable

Example:

{
  "success": false,
  "error": {
    "code": "SERVICE_UNAVAILABLE",
    "message": "Service temporarily unavailable",
    "details": {
      "reason": "database_maintenance",
      "retry_after": 300,
      "estimated_completion": "2024-01-16T15:00:00Z"
    }
  }
}

HTTP Headers:

HTTP/1.1 503 Service Unavailable
Retry-After: 300

WebSocket-Specific Errors

CONNECTION_LIMIT_EXCEEDED

Meaning: Too many concurrent WebSocket connections

Example:

{
  "type": "error",
  "error": {
    "code": "CONNECTION_LIMIT_EXCEEDED",
    "message": "Maximum 5 concurrent connections per user",
    "details": {
      "limit": 5,
      "current": 5
    }
  }
}

How to fix:

  • Close unused connections
  • Implement connection pooling
  • Use fewer browser tabs

SUBSCRIPTION_LIMIT_EXCEEDED

Meaning: Too many subscriptions per connection

Example:

{
  "type": "error",
  "subscription_id": "sub_99",
  "error": {
    "code": "SUBSCRIPTION_LIMIT_EXCEEDED",
    "message": "Maximum 50 subscriptions per connection",
    "details": {
      "limit": 50,
      "current": 50
    }
  }
}

How to fix:

  • Unsubscribe from unused subscriptions
  • Use broader filters instead of many narrow subscriptions
  • Combine related subscriptions

INVALID_MESSAGE

Meaning: WebSocket message is malformed

Example:

{
  "type": "error",
  "error": {
    "code": "INVALID_MESSAGE",
    "message": "Invalid JSON in WebSocket message",
    "details": {
      "reason": "unexpected_token",
      "position": 42
    }
  }
}

Error Response Examples

Validation Error (Multiple Fields)

Request:

POST /api/v1/data/account
Content-Type: application/json

{
  "name": "",
  "email": "not-an-email",
  "revenue": -1000
}

Response:

HTTP/1.1 400 Bad Request
Content-Type: application/json

{
  "success": false,
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Validation failed for 3 fields",
    "details": {
      "fields": [
        {
          "field": "name",
          "message": "Name is required",
          "constraint": "required",
          "value": ""
        },
        {
          "field": "email",
          "message": "Invalid email format",
          "constraint": "format",
          "value": "not-an-email"
        },
        {
          "field": "revenue",
          "message": "Revenue must be positive",
          "constraint": "min",
          "value": -1000,
          "expected": 0
        }
      ]
    },
    "request_id": "req_abc123",
    "timestamp": "2024-01-16T14:30:00Z"
  }
}

Permission Denied

Request:

DELETE /api/v1/data/account/acc_123
Authorization: Bearer <token>

Response:

HTTP/1.1 403 Forbidden
Content-Type: application/json

{
  "success": false,
  "error": {
    "code": "FORBIDDEN",
    "message": "Insufficient permissions to delete accounts",
    "details": {
      "resource": "account",
      "resource_id": "acc_123",
      "required_permission": "account:delete",
      "user_permissions": ["account:read", "account:write"],
      "hint": "Contact your administrator to request delete permission"
    },
    "request_id": "req_def456",
    "timestamp": "2024-01-16T14:35:00Z"
  }
}

Rate Limit Exceeded

Request:

GET /api/v1/data/task
Authorization: Bearer <token>

Response:

HTTP/1.1 429 Too Many Requests
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1705412460
Retry-After: 45
Content-Type: application/json

{
  "success": false,
  "error": {
    "code": "RATE_LIMITED",
    "message": "Rate limit exceeded: 1000 requests per minute",
    "details": {
      "limit": 1000,
      "window": "1m",
      "retry_after": 45,
      "quota_reset": "2024-01-16T14:31:00Z",
      "upgrade_url": "https://app.acme.com/billing/upgrade"
    },
    "request_id": "req_ghi789",
    "timestamp": "2024-01-16T14:30:15Z"
  }
}

Error Handling Best Practices

✅ Use Error Codes, Not Messages

Bad:

if (error.message.includes('not found')) {
  // Brittle - breaks if message changes
}

Good:

if (error.code === 'NOT_FOUND') {
  // Reliable - code never changes
}

✅ Show User-Friendly Messages

Bad:

alert(error.message);  // "VALIDATION_ERROR: Field 'email' constraint 'format' failed"

Good:

const userMessages = {
  'VALIDATION_ERROR': 'Please check your input and try again',
  'UNAUTHORIZED': 'Please log in to continue',
  'RATE_LIMITED': 'Too many requests. Please wait a moment.',
};

showToast(userMessages[error.code] || 'An error occurred');

✅ Handle Field-Specific Validation Errors

Good:

async function handleSubmit(data) {
  try {
    const response = await api.createAccount(data);
    return response.data;
  } catch (error) {
    if (error.code === 'VALIDATION_ERROR') {
      // Show errors next to fields
      error.details.fields.forEach(({ field, message }) => {
        setFieldError(field, message);
      });
    } else {
      // Show general error
      showToast(error.message, 'error');
    }
    throw error;
  }
}

✅ Implement Retry Logic

Good:

async function fetchWithRetry(url, options = {}, maxRetries = 3) {
  let lastError;
  
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      const response = await fetch(url, options);
      const data = await response.json();
      
      if (!response.ok) {
        // Check if error is retryable
        if (data.error.code === 'RATE_LIMITED') {
          const retryAfter = data.error.details.retry_after || 1;
          await sleep(retryAfter * 1000);
          continue;
        } else if (data.error.code === 'SERVER_ERROR') {
          // Exponential backoff
          await sleep(Math.pow(2, attempt) * 1000);
          continue;
        } else {
          // Don't retry validation errors, auth errors, etc.
          throw new APIError(data.error);
        }
      }
      
      return data;
    } catch (error) {
      lastError = error;
    }
  }
  
  throw lastError;
}

✅ Log Request IDs for Debugging

Good:

try {
  await api.createAccount(data);
} catch (error) {
  console.error('Account creation failed', {
    request_id: error.request_id,
    code: error.code,
    message: error.message,
    timestamp: error.timestamp
  });
  
  // Send to error tracking service
  Sentry.captureException(error, {
    extra: { request_id: error.request_id }
  });
}

✅ Handle Network Errors

Good:

try {
  const response = await fetch('/api/v1/data/task');
  const data = await response.json();
  
  if (!data.success) {
    throw new APIError(data.error);
  }
  
  return data.data;
} catch (error) {
  if (error instanceof TypeError && error.message === 'Failed to fetch') {
    // Network error - server unreachable
    showToast('Network error. Please check your connection.', 'error');
  } else if (error instanceof APIError) {
    // API returned error
    handleAPIError(error);
  } else {
    // Unknown error
    console.error('Unexpected error:', error);
    showToast('An unexpected error occurred', 'error');
  }
}

Debugging with Request IDs

Every API response includes a request_id for debugging:

Request:

POST /api/v1/data/account
Content-Type: application/json
X-Request-ID: my-custom-id-123

{ "name": "Acme Corp" }

Response:

{
  "success": true,
  "data": { ... },
  "request_id": "my-custom-id-123"
}

Server logs:

[2024-01-16T14:30:00Z] INFO [my-custom-id-123] POST /api/v1/data/account
[2024-01-16T14:30:00Z] DEBUG [my-custom-id-123] Validating input
[2024-01-16T14:30:00Z] DEBUG [my-custom-id-123] Executing query: INSERT INTO accounts...
[2024-01-16T14:30:00Z] INFO [my-custom-id-123] Request completed in 45ms

Use request IDs to:

  • Trace request through logs
  • Debug distributed systems
  • Report issues to support
  • Correlate frontend errors with backend logs

Monitoring & Alerting

Error Rate Monitoring

Track error rates by code:

// Metrics dashboard
{
  "error_rates": {
    "VALIDATION_ERROR": 0.05,  // 5% of requests
    "UNAUTHORIZED": 0.02,      // 2% of requests
    "RATE_LIMITED": 0.01,      // 1% of requests
    "SERVER_ERROR": 0.0001     // 0.01% of requests (🚨 alert if > 0.01%)
  }
}

Alert Conditions

Critical alerts:

  • SERVER_ERROR rate > 0.1%
  • SERVICE_UNAVAILABLE > 0
  • Database connection failures

Warning alerts:

  • RATE_LIMITED spike (may indicate DDoS or integration bug)
  • UNAUTHORIZED spike (credential leakage?)
  • VALIDATION_ERROR spike on new form (bad client-side validation)

Error Budgets

Set error budgets per service:

error_budget:
  target_success_rate: 99.9%
  measurement_window: 30d
  budget_remaining: 97.2%  # Still have 97.2% of error budget left

When budget exhausted:

  • Freeze feature releases
  • Focus on reliability improvements
  • Investigate root causes

Security Considerations

❌ Never Leak Sensitive Info

Bad:

{
  "error": {
    "code": "UNAUTHORIZED",
    "message": "Password incorrect for user john@acme.com",
    "details": {
      "attempted_password": "Password123!",  // 🚨 NEVER DO THIS
      "actual_password_hash": "bcrypt$..."    // 🚨 NEVER DO THIS
    }
  }
}

Good:

{
  "error": {
    "code": "UNAUTHORIZED",
    "message": "Invalid credentials",
    "details": null
  }
}

❌ Don't Confirm Resource Existence

Bad:

// Attacker probes: DELETE /api/v1/data/account/acc_123
{
  "error": {
    "code": "FORBIDDEN",
    "message": "You don't have permission to delete this account"
  }
}
// Attacker learns: Account acc_123 exists! 🚨

Good:

// Return NOT_FOUND for both "doesn't exist" and "exists but no permission"
{
  "error": {
    "code": "NOT_FOUND",
    "message": "Account not found"
  }
}

✅ Rate Limit Error Responses

Even error responses can be abused:

// Attacker tries to enumerate user emails
for (let i = 0; i < 1000000; i++) {
  await register({ email: `user${i}@example.com` });
  // Response: "ALREADY_EXISTS" or "VALIDATION_ERROR"
}

Solution: Rate limit failed registration attempts:

{
  "error": {
    "code": "RATE_LIMITED",
    "message": "Too many failed registration attempts"
  }
}

Next Steps

On this page