Error Handling

Comprehensive guide to error handling patterns, standard response formats, HTTP status codes, and best practices for resilient API integration.

Updated Dec 16, 2025
Edit on GitHub
errors http-status debugging best-practices

Error Handling

This guide covers error handling patterns, standard response formats, HTTP status codes, and best practices for building resilient applications with the AuthOS API.

Overview

The AuthOS API uses standard HTTP status codes and returns consistent, structured error responses. Understanding these patterns helps you build robust error handling into your applications.

Key Principles

  • Consistent Format: All errors follow the same JSON structure
  • Descriptive Messages: Human-readable error messages for debugging
  • Machine-Readable Codes: Structured error codes for programmatic handling
  • HTTP Standards: Proper use of HTTP status codes
  • Timestamp Tracking: Every error includes a timestamp for correlation

Error Response Format

All API errors return a JSON object with the following structure:

{
  "error": "Human-readable error message describing what went wrong",
  "error_code": "MACHINE_READABLE_ERROR_CODE",
  "timestamp": "2025-01-15T10:30:00Z"
}

Fields

Field Type Description
error string Human-readable description of the error
error_code string Machine-readable error code in SCREAMING_SNAKE_CASE
timestamp string ISO 8601 timestamp when the error occurred

Example

{
  "error": "Organization not found",
  "error_code": "NOT_FOUND",
  "timestamp": "2025-01-15T10:30:00.123Z"
}

HTTP Status Codes

The API uses standard HTTP status codes to indicate the general category of error.

2xx Success

Code Meaning Description
200 OK Success Request succeeded and response contains data
201 Created Resource Created New resource created successfully
204 No Content Success (No Response Body) Request succeeded, no content to return

4xx Client Errors

Code Meaning When It Occurs
400 Bad Request Invalid Request Malformed JSON, invalid parameters, business logic violation
401 Unauthorized Authentication Failed Missing, invalid, or expired JWT/API key
403 Forbidden Authorization Failed Valid credentials but insufficient permissions
404 Not Found Resource Not Found Requested resource doesn’t exist
409 Conflict Resource Conflict Resource already exists or state conflict
429 Too Many Requests Rate Limited Exceeded rate limit for endpoint

5xx Server Errors

Code Meaning When It Occurs
500 Internal Server Error Server Error Unexpected server-side error
503 Service Unavailable Service Down API temporarily unavailable (maintenance, overload)

Error Codes Reference

General Errors

Error Code HTTP Status Description Common Causes
BAD_REQUEST 400 Invalid request parameters Malformed JSON, missing required fields, invalid data types
UNAUTHORIZED 401 Authentication failed Missing token, invalid token, malformed Authorization header
FORBIDDEN 403 Insufficient permissions Valid credentials but wrong role/permissions
NOT_FOUND 404 Resource not found Invalid ID, deleted resource, wrong endpoint
INTERNAL_SERVER_ERROR 500 Server error Database error, unexpected exception

Authentication & Authorization Errors

Error Code HTTP Status Description Resolution
UNAUTHORIZED 401 Missing or invalid credentials Provide valid JWT or API key
TOKEN_EXPIRED 401 JWT has expired Refresh token using /api/auth/refresh
JWT_ERROR 401 Invalid JWT signature or format Obtain new token via authentication
FORBIDDEN 403 Insufficient permissions Request access or use account with proper role
ORGANIZATION_NOT_ACTIVE 403 Organization suspended or pending Contact platform admin

Business Logic Errors

Error Code HTTP Status Description Resolution
SERVICE_LIMIT_EXCEEDED 400 Max services reached for org tier Upgrade tier or delete unused services
TEAM_LIMIT_EXCEEDED 400 Max team members reached Upgrade tier or remove members
INVITATION_EXPIRED 400 Invitation link expired Request new invitation
DEVICE_CODE_EXPIRED 400 Device authorization code expired Start new device flow
DEVICE_CODE_PENDING 400 User hasn’t authorized device yet Continue polling

Database & Infrastructure Errors

Error Code HTTP Status Description Resolution
DATABASE_ERROR 500 Database operation failed Retry request; contact support if persists
OAUTH_ERROR 500 OAuth provider error Check provider status; retry later
STRIPE_ERROR 500 Billing system error Check Stripe status; contact support
AUDIT_ERROR 500 Audit logging failed Request succeeded but not logged; report to support

Common Error Scenarios

Authentication Errors

Missing Authorization Header

Request:

curl -X GET https://sso.example.com/api/user
# Missing: -H "Authorization: Bearer {jwt}"

Response (401 Unauthorized):

{
  "error": "Missing or invalid Authorization header",
  "error_code": "UNAUTHORIZED",
  "timestamp": "2025-01-15T10:30:00Z"
}

Solution: Include Authorization: Bearer {jwt} header in request.


Expired JWT Token

Request:

curl -X GET https://sso.example.com/api/user \
  -H "Authorization: Bearer {expired_jwt}"

Response (401 Unauthorized):

{
  "error": "Token expired",
  "error_code": "TOKEN_EXPIRED",
  "timestamp": "2025-01-15T10:30:00Z"
}

Solution: Use refresh token to get new access token:

curl -X POST https://sso.example.com/api/auth/refresh \
  -H "Content-Type: application/json" \
  -d '{"refresh_token": "your-refresh-token"}'

Invalid API Key

Request:

curl -X GET https://sso.example.com/api/service/users \
  -H "X-API-Key: sk_invalid_key"

Response (401 Unauthorized):

{
  "error": "Invalid API key",
  "error_code": "UNAUTHORIZED",
  "timestamp": "2025-01-15T10:30:00Z"
}

Solution: Verify API key is correct and hasn’t been revoked.


Authorization Errors

Insufficient Permissions

Request: Platform owner endpoint without platform owner role

curl -X GET https://sso.example.com/api/platform/organizations \
  -H "Authorization: Bearer {non_platform_owner_jwt}"

Response (403 Forbidden):

{
  "error": "Platform owner access required",
  "error_code": "FORBIDDEN",
  "timestamp": "2025-01-15T10:30:00Z"
}

Solution: Request platform owner access or use proper admin account.


Organization Not Active

Request: Attempting to authenticate with suspended organization

curl -X GET https://sso.example.com/auth/github?org=suspended-org&service=app

Response (403 Forbidden):

{
  "error": "Organization is not active",
  "error_code": "ORGANIZATION_NOT_ACTIVE",
  "timestamp": "2025-01-15T10:30:00Z"
}

Solution: Contact platform administrator to reactivate organization.


Validation Errors

Missing Required Field

Request:

curl -X POST https://sso.example.com/api/service/users \
  -H "X-API-Key: sk_live_abc123" \
  -H "Content-Type: application/json" \
  -d '{}'

Response (400 Bad Request):

{
  "error": "Missing required field: email",
  "error_code": "BAD_REQUEST",
  "timestamp": "2025-01-15T10:30:00Z"
}

Solution: Include all required fields in request body.


Invalid Email Format

Request:

curl -X POST https://sso.example.com/api/service/users \
  -H "X-API-Key: sk_live_abc123" \
  -H "Content-Type: application/json" \
  -d '{"email": "not-an-email"}'

Response (400 Bad Request):

{
  "error": "Invalid email format",
  "error_code": "BAD_REQUEST",
  "timestamp": "2025-01-15T10:30:00Z"
}

Solution: Provide a valid email address.


Resource Errors

Resource Not Found

Request:

curl -X GET https://sso.example.com/api/service/users/invalid-user-id \
  -H "X-API-Key: sk_live_abc123"

Response (404 Not Found):

{
  "error": "User not found or has not authenticated with this service",
  "error_code": "NOT_FOUND",
  "timestamp": "2025-01-15T10:30:00Z"
}

Solution: Verify the resource ID and ensure user has authenticated with your service.


Resource Limit Exceeded

Request: Creating service when at tier limit

curl -X POST https://sso.example.com/api/organizations/acme-corp/services \
  -H "Authorization: Bearer {jwt}" \
  -d '{"name": "New Service", "slug": "new-svc"}'

Response (400 Bad Request):

{
  "error": "Service limit exceeded for organization tier",
  "error_code": "SERVICE_LIMIT_EXCEEDED",
  "timestamp": "2025-01-15T10:30:00Z"
}

Solution: Upgrade organization tier or delete unused services.


Rate Limiting

Too Many Requests

Request: Exceeding rate limit

# 101st request within rate limit window
curl -X GET https://sso.example.com/api/service/users \
  -H "X-API-Key: sk_live_abc123"

Response (429 Too Many Requests):

{
  "error": "Rate limit exceeded",
  "error_code": "RATE_LIMIT_EXCEEDED",
  "timestamp": "2025-01-15T10:30:00Z"
}

Headers:

Retry-After: 60

Solution: Wait for rate limit reset or implement exponential backoff.

See Also: Rate Limiting Concepts for detailed rate limit policies and best practices.


Error Handling Best Practices

1. Check HTTP Status First

Always check the HTTP status code before parsing the response body:

async function makeRequest(url, options) {
  const response = await fetch(url, options);
  
  if (!response.ok) {
    const error = await response.json();
    throw new APIError(response.status, error);
  }
  
  return response.json();
}

class APIError extends Error {
  constructor(status, errorBody) {
    super(errorBody.error);
    this.status = status;
    this.code = errorBody.error_code;
    this.timestamp = errorBody.timestamp;
  }
}

2. Handle Token Expiration Automatically

Implement automatic token refresh on TOKEN_EXPIRED errors:

async function apiRequestWithRefresh(url, options) {
  let response = await fetch(url, options);
  
  if (response.status === 401) {
    const error = await response.json();
    
    if (error.error_code === 'TOKEN_EXPIRED') {
      // Refresh token
      const newTokens = await refreshAccessToken();
      
      // Retry original request with new token
      options.headers['Authorization'] = `Bearer ${newTokens.access_token}`;
      response = await fetch(url, options);
    }
  }
  
  return response;
}

3. Implement Exponential Backoff for Rate Limits

Use exponential backoff when encountering rate limits:

async function fetchWithBackoff(url, options, maxRetries = 3) {
  for (let i = 0; i < maxRetries; i++) {
    const response = await fetch(url, options);
    
    if (response.status === 429) {
      const retryAfter = parseInt(response.headers.get('Retry-After') || '60');
      const backoff = Math.min(retryAfter * 1000, 2 ** i * 1000);
      
      console.log(`Rate limited. Retrying after ${backoff}ms...`);
      await new Promise(resolve => setTimeout(resolve, backoff));
      continue;
    }
    
    return response;
  }
  
  throw new Error('Max retries exceeded');
}

4. Handle Network Errors Separately

Distinguish between API errors and network failures:

async function apiRequest(url, options) {
  try {
    const response = await fetch(url, options);
    
    if (!response.ok) {
      const error = await response.json();
      throw new APIError(response.status, error);
    }
    
    return response.json();
  } catch (error) {
    if (error instanceof APIError) {
      // API returned an error response
      console.error('API Error:', error.code, error.message);
      throw error;
    } else {
      // Network error (timeout, DNS failure, etc.)
      console.error('Network Error:', error.message);
      throw new NetworkError(error);
    }
  }
}

5. Log Errors with Context

Include request context when logging errors:

async function apiRequest(url, options) {
  const requestId = generateRequestId();
  
  try {
    const response = await fetch(url, options);
    
    if (!response.ok) {
      const error = await response.json();
      
      console.error('API Request Failed', {
        requestId,
        url,
        method: options.method,
        status: response.status,
        errorCode: error.error_code,
        errorMessage: error.error,
        timestamp: error.timestamp
      });
      
      throw new APIError(response.status, error);
    }
    
    return response.json();
  } catch (error) {
    if (!(error instanceof APIError)) {
      console.error('Request Failed', {
        requestId,
        url,
        method: options.method,
        error: error.message
      });
    }
    throw error;
  }
}

6. Display User-Friendly Messages

Don’t expose raw error messages to end users:

function getUserFriendlyMessage(error) {
  const friendlyMessages = {
    'TOKEN_EXPIRED': 'Your session has expired. Please log in again.',
    'UNAUTHORIZED': 'Authentication failed. Please log in.',
    'FORBIDDEN': 'You don\'t have permission to perform this action.',
    'NOT_FOUND': 'The requested resource could not be found.',
    'SERVICE_LIMIT_EXCEEDED': 'You\'ve reached your service limit. Please upgrade your plan.',
    'RATE_LIMIT_EXCEEDED': 'Too many requests. Please try again in a moment.',
    'INTERNAL_SERVER_ERROR': 'Something went wrong. Please try again later.'
  };
  
  return friendlyMessages[error.code] || 'An unexpected error occurred.';
}

7. Validate Before Sending

Validate input client-side before making API requests:

function validateEmail(email) {
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  if (!emailRegex.test(email)) {
    throw new ValidationError('Invalid email format');
  }
}

async function createUser(email) {
  // Validate first to avoid unnecessary API call
  validateEmail(email);
  
  return apiRequest('/api/service/users', {
    method: 'POST',
    headers: {
      'X-API-Key': process.env.SSO_API_KEY,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({ email })
  });
}

8. Handle Specific Error Codes

Implement specific handlers for different error codes:

async function handleAPIError(error) {
  switch (error.code) {
    case 'TOKEN_EXPIRED':
      await refreshAndRetry();
      break;
      
    case 'ORGANIZATION_NOT_ACTIVE':
      redirectToSuspendedPage();
      break;
      
    case 'SERVICE_LIMIT_EXCEEDED':
      showUpgradeModal();
      break;
      
    case 'RATE_LIMIT_EXCEEDED':
      await retryWithBackoff();
      break;
      
    default:
      showGenericError(getUserFriendlyMessage(error));
  }
}

Testing Error Scenarios

Unit Testing Error Handling

describe('API Error Handling', () => {
  it('should refresh token on TOKEN_EXPIRED', async () => {
    // Mock expired token response
    fetchMock.mockResponseOnce(
      JSON.stringify({
        error: 'Token expired',
        error_code: 'TOKEN_EXPIRED',
        timestamp: new Date().toISOString()
      }),
      { status: 401 }
    );
    
    // Mock successful refresh
    fetchMock.mockResponseOnce(
      JSON.stringify({
        access_token: 'new_token',
        refresh_token: 'new_refresh_token'
      })
    );
    
    // Mock successful retry
    fetchMock.mockResponseOnce(
      JSON.stringify({ id: 'user_123' })
    );
    
    const result = await apiRequestWithRefresh('/api/user');
    expect(result.id).toBe('user_123');
  });
  
  it('should handle rate limiting with backoff', async () => {
    // Mock rate limit response
    fetchMock.mockResponseOnce('', {
      status: 429,
      headers: { 'Retry-After': '2' }
    });
    
    // Mock successful retry
    fetchMock.mockResponseOnce(
      JSON.stringify({ users: [] })
    );
    
    const result = await fetchWithBackoff('/api/service/users');
    expect(result.users).toEqual([]);
  });
});

Integration Testing

Test error scenarios in integration tests:

describe('Service API Integration', () => {
  it('should return 403 for insufficient permissions', async () => {
    const apiKey = await createAPIKey(['read:users']); // No write permission
    
    const response = await fetch(`${API_URL}/api/service/users`, {
      method: 'POST',
      headers: {
        'X-API-Key': apiKey,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({ email: 'test@example.com' })
    });
    
    expect(response.status).toBe(403);
    const error = await response.json();
    expect(error.error_code).toBe('FORBIDDEN');
  });
});

Debugging Tips

1. Use Timestamps for Correlation

Error timestamps help correlate client-side errors with server logs:

const error = await response.json();
console.error(`Error at ${error.timestamp}:`, error.error);
// Search server logs around this timestamp

2. Check Rate Limit Headers

Monitor rate limit headers to prevent hitting limits:

const response = await fetch(url, options);
console.log('Rate Limit:', {
  limit: response.headers.get('X-RateLimit-Limit'),
  remaining: response.headers.get('X-RateLimit-Remaining'),
  reset: new Date(parseInt(response.headers.get('X-RateLimit-Reset')) * 1000)
});

3. Enable Request Logging

Log all API requests for debugging:

async function apiRequest(url, options) {
  const startTime = Date.now();
  console.log('API Request:', { url, method: options.method });
  
  try {
    const response = await fetch(url, options);
    const duration = Date.now() - startTime;
    
    console.log('API Response:', {
      url,
      status: response.status,
      duration: `${duration}ms`
    });
    
    return response;
  } catch (error) {
    const duration = Date.now() - startTime;
    console.error('API Request Failed:', {
      url,
      duration: `${duration}ms`,
      error: error.message
    });
    throw error;
  }
}

4. Use Request IDs

Generate unique request IDs for tracing:

function generateRequestId() {
  return `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}

async function apiRequest(url, options) {
  const requestId = generateRequestId();
  options.headers['X-Request-ID'] = requestId;
  
  console.log(`[${requestId}] Request:`, url);
  
  try {
    const response = await fetch(url, options);
    console.log(`[${requestId}] Response:`, response.status);
    return response;
  } catch (error) {
    console.error(`[${requestId}] Error:`, error.message);
    throw error;
  }
}

Error Recovery Patterns

Automatic Retry with Circuit Breaker

Implement circuit breaker pattern to prevent cascading failures:

class CircuitBreaker {
  constructor(threshold = 5, timeout = 60000) {
    this.failureCount = 0;
    this.threshold = threshold;
    this.timeout = timeout;
    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.failureCount = 0;
    this.state = 'CLOSED';
  }
  
  onFailure() {
    this.failureCount++;
    if (this.failureCount >= this.threshold) {
      this.state = 'OPEN';
      this.nextAttempt = Date.now() + this.timeout;
    }
  }
}

const breaker = new CircuitBreaker();

async function makeRequest(url, options) {
  return breaker.execute(() => fetch(url, options));
}

Graceful Degradation

Provide fallback behavior when API is unavailable:

async function getUserData(userId) {
  try {
    return await apiRequest(`/api/service/users/${userId}`);
  } catch (error) {
    if (error.status >= 500) {
      // Server error - use cached data if available
      const cached = localStorage.getItem(`user_${userId}`);
      if (cached) {
        console.warn('Using cached user data due to server error');
        return JSON.parse(cached);
      }
    }
    throw error;
  }
}

Summary

Quick Reference

Always:

  • Check HTTP status code first
  • Parse error response JSON for details
  • Log errors with context
  • Show user-friendly messages

Never:

  • Ignore error responses
  • Expose raw errors to users
  • Retry indefinitely
  • Commit API keys to version control

Handle Specifically:

  • TOKEN_EXPIRED: Refresh and retry
  • RATE_LIMIT_EXCEEDED: Backoff and retry
  • ORGANIZATION_NOT_ACTIVE: Redirect to status page
  • SERVICE_LIMIT_EXCEEDED: Show upgrade prompt

Additional Resources