Error Handling

Babel Shield throws typed errors with properties that help you handle each failure mode correctly.

Error Types

Error Status Retryable Key Properties
ValidationError 400/422 No code, errors.field
RateLimitError 429 Yes retryAfter, rateLimitCode, clientSide
NetworkError -- Yes possibleCors
TimeoutError -- Yes timeout (ms)
AuthError 401/402/403 No code, status
ServerError 5xx Yes status

Every error has a retryable property. Only retry when retryable is true.

Handling Errors

Use error.name to identify the type and access its specific properties:

try {
  const result = await BabelShield.moderate(content);
}
catch (error) {
  switch (error.name) {
    case 'ValidationError':
      // Payload is invalid -- fix before retrying
      console.log('Field:', error.errors?.field);
      break;

    case 'RateLimitError':
      // Too many requests -- wait before retrying
      console.log('Retry after:', error.retryAfter, 'seconds');
      console.log('API error code:', error.rateLimitCode);
      console.log('Client-side limit:', error.clientSide);
      break;

    case 'NetworkError':
      // Network failure -- check connectivity
      if (error.possibleCors) {
        console.log('Possible CORS issue -- check domain authorization');
      }
      break;

    case 'TimeoutError':
      // Request timed out
      console.log('Timed out after:', error.timeout, 'ms');
      break;

    case 'AuthError':
      // Authentication failed -- check token
      console.log('Auth error:', error.code);
      // Codes: 'invalid_token', 'subscription_required', 'forbidden'
      break;

    case 'ServerError':
      // Server-side error -- retry
      console.log('Server error:', error.status);
      break;
  }
}

Retry with Exponential Backoff

For retryable errors, use exponential backoff with jitter to avoid thundering herd problems:

async function retryWithBackoff(fn, maxRetries = 3, baseDelay = 1000) {
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      return await fn();
    }
    catch (error) {
      if (!error.retryable || attempt === maxRetries) {
        throw error;
      }

      const delay = baseDelay * Math.pow(2, attempt - 1);
      const jitter = Math.random() * 200;
      const wait = Math.min(delay + jitter, 10000);

      console.log(`Retry ${attempt}/${maxRetries} in ${Math.round(wait)}ms`);
      await new Promise(resolve => setTimeout(resolve, wait));
    }
  }
}

// Usage
try {
  const result = await retryWithBackoff(() =>
    BabelShield.moderate(content)
  );
}
catch (error) {
  console.error('All retries failed:', error.name, error.message);
}

The built-in API client already retries automatically on timeout, 429, 500, and 503 status codes (up to 2 retries). Use the retryWithBackoff pattern for additional retry control in your application layer.

Rate-Limit Hints (retryAfter and rateLimitCode)

On a 429, RateLimitError surfaces two hints from the API (matching the PHP library):

  • retryAfter -- seconds to wait before retrying, or null when the server sent no usable hint. The value is resolved in this order: Retry-After header, then X-RateLimit-Reset header, then the response body's errors.retry_after. All sources are interpreted as relative seconds (never a timestamp), and the surfaced value is capped at 86,400 seconds (24 hours). Unparseable or negative values are ignored.
  • rateLimitCode -- the API's machine-readable 429 error_code (for example RATE_LIMIT_EXCEEDED on /moderate, or THROTTLE_LIMIT_EXCEEDED on the auth/token endpoints), or null. Treat it as an opaque string for logging and branching; it is length-capped at 64 characters and distinct from error.code, which is always 'rate_limit'.

Do not blindly sleep() on the hint. retryAfter can legitimately be hours long (up to the 24-hour cap) when an account-level quota is exhausted -- pausing a browser session that long is never useful. Treat large values as "stop retrying and degrade gracefully" rather than as a literal wait. The built-in automatic retry already does this: its per-attempt delay is clamped to at most 10 seconds regardless of retryAfter.

Fail-Open Behavior

By default, failOpen is true. If moderation fails for any reason, forms submit normally so users are never blocked by a service outage.

const result = await BabelShield.init({
  apiToken: 'YOUR_API_TOKEN',
  failOpen: true
});

if (result.failOpen) {
  // Initialization failed, but forms still work
  console.warn('Moderation unavailable:', result.message);
}

Set failOpen: false to block form submissions when moderation is unavailable. Use this for high-risk forms where unmoderated content is not acceptable:

BabelShield.init({
  apiToken: 'YOUR_API_TOKEN',
  failOpen: false
});

Event-Based Error Monitoring

Use the event system for centralized error logging without wrapping every call in try-catch:

// Monitor all API errors
BabelShield.on('api:error', ({ error }) => {
  console.error('API Error:', error.name, error.message);
  // Send to your error tracking service
  errorTracker.capture(error);
});

// Monitor all API responses
BabelShield.on('api:response', (result) => {
  console.log('Response received:', result.cached ? '(cached)' : '(fresh)');
});

Events fire regardless of whether the caller handles the error. This is useful for metrics, alerting, and centralized logging alongside per-call error handling.

Next Steps