Skip to content

Error Handling Guide

This guide explains how to handle errors when working with the Centrali API and SDK.

Error Response Format

All Centrali API errors follow a consistent JSON format:

{
  "error": "Error Type",
  "message": "Human-readable error description",
  "details": [
    {
      "field": "fieldName",
      "message": "Field-specific error message"
    }
  ],
  "code": "ERROR_CODE",
  "requestId": "req_abc123"
}

Fields: - error: Short error type (e.g., "Validation Error", "Unauthorized") - message: Detailed error description - details: Array of field-level errors (for validation failures) - code: Machine-readable error code (optional) - requestId: Unique request identifier for debugging


HTTP Status Codes

2xx - Success

200 OK

Request succeeded.

{
  "data": { ... },
  "id": "rec_abc123"
}

201 Created

Resource created successfully.

{
  "id": "str_abc123",
  "name": "Product",
  "createdAt": "2024-01-15T10:30:00Z"
}

204 No Content

Request succeeded with no response body (e.g., delete operations).


4xx - Client Errors

400 Bad Request - Validation Failed

Cause: Request data doesn't match schema requirements.

Response:

{
  "error": "Validation Failed",
  "message": "The request contains invalid data",
  "details": [
    {
      "field": "email",
      "message": "Invalid email format"
    },
    {
      "field": "price",
      "message": "Must be greater than or equal to 0"
    }
  ]
}

Solution: - Check field types match structure definition - Verify required fields are present - Validate format constraints (email, URL, etc.) - Check min/max constraints for numbers and strings

Example:

try {
  await centrali.createRecord('User', {
    email: 'invalid-email',  // ❌ Invalid format
    age: -5                   // ❌ Negative number
  });
} catch (error) {
  if (error.response?.status === 400) {
    console.error('Validation errors:', error.response.data.details);
    // Output: [{ field: 'email', message: 'Invalid email format' }, ...]
  }
}


401 Unauthorized - Invalid or Missing Token

Cause: Missing, invalid, or expired authentication token.

Response:

{
  "error": "Unauthorized",
  "message": "Invalid or expired authentication token"
}

Common Causes: 1. Missing Authorization header

# ❌ Wrong
curl https://api.centrali.io/data/workspace/my-workspace/api/v1/structures

# ✅ Correct
curl https://api.centrali.io/data/workspace/my-workspace/api/v1/structures \
  -H "Authorization: Bearer YOUR_TOKEN"

  1. Token expired (lifetime: 7 hours)
  2. Fetch a new token from /oidc/token
  3. SDK handles this automatically

  4. Invalid credentials

  5. Verify client_id and client_secret
  6. Check for extra spaces or newlines

Solution:

// With SDK (handles automatically)
const centrali = new CentraliSDK({
  clientId: process.env.CENTRALI_CLIENT_ID,
  clientSecret: process.env.CENTRALI_CLIENT_SECRET,
  // ... other config
});

// Manual handling
async function fetchWithAuth(url) {
  try {
    const response = await fetch(url, {
      headers: { 'Authorization': `Bearer ${token}` }
    });

    if (response.status === 401) {
      // Token expired - fetch new one
      token = await getNewToken();
      // Retry request
      return fetch(url, {
        headers: { 'Authorization': `Bearer ${token}` }
      });
    }

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


403 Forbidden - Insufficient Permissions

Cause: Token is valid but lacks required permissions.

Response:

{
  "error": "Forbidden",
  "message": "You do not have permission to perform this action"
}

Solution: - Verify service account has required permissions - Check workspace access - Ensure you're using the correct workspace credentials


404 Not Found - Resource Doesn't Exist

Cause: Requested resource doesn't exist or you don't have access.

Response:

{
  "error": "Not Found",
  "message": "Structure 'str_xyz789' does not exist"
}

Common Scenarios: - Structure ID doesn't exist - Record ID doesn't exist - Wrong workspace - Resource was deleted

Solution:

try {
  const record = await centrali.getRecord('Product', 'rec_nonexistent');
} catch (error) {
  if (error.response?.status === 404) {
    console.error('Record not found');
    // Handle gracefully (show error message, redirect, etc.)
  }
}


409 Conflict - Resource Already Exists

Cause: Attempting to create a resource that violates a uniqueness constraint.

Response:

{
  "error": "Conflict",
  "message": "A record with slug 'my-product' already exists",
  "details": [
    {
      "field": "slug",
      "message": "Value must be unique"
    }
  ]
}

Solution: - Use a different value for unique fields - Check if resource exists before creating - Use update instead of create


422 Unprocessable Entity - Business Logic Error

Cause: Request is valid but cannot be processed due to business logic.

Response:

{
  "error": "Unprocessable Entity",
  "message": "Cannot delete structure with existing records"
}

Example Scenarios: - Deleting a structure that has records - Circular reference in relationships - Violating custom validation rules


429 Too Many Requests - Rate Limit Exceeded

Cause: Exceeded API rate limits.

Response:

{
  "error": "Too Many Requests",
  "message": "Rate limit exceeded",
  "retryAfter": 60
}

Headers:

X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1705351200
Retry-After: 60

Solution:

async function makeRequestWithRetry(fn, maxRetries = 3) {
  for (let i = 0; i < maxRetries; i++) {
    try {
      return await fn();
    } catch (error) {
      if (error.response?.status === 429) {
        const retryAfter = error.response.headers['retry-after'] || 60;
        console.log(`Rate limited. Retrying in ${retryAfter}s...`);
        await new Promise(resolve => setTimeout(resolve, retryAfter * 1000));
        continue;
      }
      throw error;
    }
  }
  throw new Error('Max retries exceeded');
}

See Limits & Quotas for rate limit details.


5xx - Server Errors

500 Internal Server Error

Cause: Unexpected server error.

Response:

{
  "error": "Internal Server Error",
  "message": "An unexpected error occurred",
  "requestId": "req_abc123"
}

Solution: - Retry the request (may be transient) - Check Centrali status page - Contact support with requestId if persistent


503 Service Unavailable

Cause: Service is temporarily unavailable (maintenance, high load).

Response:

{
  "error": "Service Unavailable",
  "message": "The service is temporarily unavailable"
}

Solution: - Implement exponential backoff retry logic - Check status page for scheduled maintenance

async function exponentialBackoff(fn, maxRetries = 5) {
  for (let i = 0; i < maxRetries; i++) {
    try {
      return await fn();
    } catch (error) {
      if (error.response?.status === 503 && i < maxRetries - 1) {
        const delay = Math.pow(2, i) * 1000; // 1s, 2s, 4s, 8s, 16s
        await new Promise(resolve => setTimeout(resolve, delay));
        continue;
      }
      throw error;
    }
  }
}

SDK Error Handling

The Centrali SDK provides typed error objects:

import { CentraliSDK } from '@centrali-io/centrali-sdk';

const centrali = new CentraliSDK({ /* config */ });

try {
  const product = await centrali.createRecord('Product', {
    name: 'Widget',
    price: 'invalid' // ❌ Wrong type
  });
} catch (error) {
  // HTTP status code
  console.log('Status:', error.response?.status);

  // Error details
  console.log('Error:', error.response?.data);

  // Handle specific errors
  if (error.response?.status === 400) {
    console.error('Validation errors:', error.response.data.details);
  } else if (error.response?.status === 401) {
    console.error('Authentication failed');
  } else if (error.response?.status === 404) {
    console.error('Resource not found');
  } else {
    console.error('Unexpected error:', error.message);
  }
}

Best Practices

1. Always Handle Errors

// ❌ Bad - Silent failure
const record = await centrali.createRecord('Product', data);

// ✅ Good - Explicit error handling
try {
  const record = await centrali.createRecord('Product', data);
  console.log('Created:', record.id);
} catch (error) {
  console.error('Failed to create record:', error.message);
  // Show user-friendly error message
  // Log error for debugging
  // Retry if appropriate
}

2. Provide User-Friendly Messages

try {
  await centrali.createRecord('Product', formData);
  showSuccess('Product created successfully!');
} catch (error) {
  if (error.response?.status === 400) {
    // Show specific validation errors
    showErrors(error.response.data.details);
  } else if (error.response?.status === 409) {
    showError('A product with this SKU already exists');
  } else {
    showError('Failed to create product. Please try again.');
  }
}

3. Log Errors for Debugging

try {
  await centrali.createRecord('Product', data);
} catch (error) {
  // Log full error details
  console.error('Create product failed:', {
    status: error.response?.status,
    error: error.response?.data,
    requestId: error.response?.data?.requestId,
    data: data
  });

  // Or use error tracking service
  Sentry.captureException(error, {
    extra: {
      requestId: error.response?.data?.requestId,
      workspace: 'my-workspace'
    }
  });
}

4. Implement Retry Logic for Transient Errors

async function createRecordWithRetry(structure, data, maxRetries = 3) {
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      return await centrali.createRecord(structure, data);
    } catch (error) {
      const status = error.response?.status;

      // Retry on 5xx errors or rate limits
      if ((status >= 500 || status === 429) && attempt < maxRetries) {
        const delay = Math.pow(2, attempt) * 1000;
        console.log(`Attempt ${attempt} failed. Retrying in ${delay}ms...`);
        await new Promise(resolve => setTimeout(resolve, delay));
        continue;
      }

      // Don't retry client errors (4xx except 429)
      throw error;
    }
  }
}

5. Validate Before Sending

// Validate on client before API call
function validateProduct(data) {
  const errors = [];

  if (!data.name || data.name.trim().length === 0) {
    errors.push({ field: 'name', message: 'Name is required' });
  }

  if (typeof data.price !== 'number' || data.price < 0) {
    errors.push({ field: 'price', message: 'Price must be a positive number' });
  }

  if (!/^[a-z0-9-]+$/.test(data.slug)) {
    errors.push({ field: 'slug', message: 'Slug must be lowercase alphanumeric' });
  }

  return errors;
}

// Use before API call
const errors = validateProduct(formData);
if (errors.length > 0) {
  showErrors(errors);
  return;
}

// Now safe to send to API
await centrali.createRecord('Product', formData);

Common Error Scenarios

Scenario 1: Creating a Record with Invalid Data

try {
  await centrali.createRecord('Product', {
    name: '', // ❌ Required field empty
    price: -10, // ❌ Negative price
    email: 'not-an-email' // ❌ Invalid format
  });
} catch (error) {
  // Status: 400
  // Error: Validation Failed
  // Details: [
  //   { field: 'name', message: 'Required field' },
  //   { field: 'price', message: 'Must be >= 0' },
  //   { field: 'email', message: 'Invalid email format' }
  // ]
}

Scenario 2: Token Expired Mid-Session

// SDK handles this automatically!
const centrali = new CentraliSDK({ clientId, clientSecret, ...});

// Even after 7 hours, SDK refetches token
const record = await centrali.getRecord('Product', 'rec_123'); // ✅ Works

Scenario 3: Network Timeout

try {
  const record = await centrali.createRecord('Product', data);
} catch (error) {
  if (error.code === 'ECONNABORTED' || error.code === 'ETIMEDOUT') {
    console.error('Request timed out. Please check your connection.');
    // Retry or show network error message
  }
}


Summary

Key Takeaways: 1. Always wrap API calls in try/catch 2. Handle specific error codes (400, 401, 404, etc.) 3. Provide user-friendly error messages 4. Log errors with requestId for debugging 5. Implement retry logic for transient errors (5xx, 429) 6. Use the SDK - it handles auth and retry automatically!