Skip to main content

Error Handling

Best practices for handling API errors gracefully.

Error Categories

CategoryStatusRecovery
Client errors4xxFix request
Rate limits429Retry with backoff
Server errors5xxRetry

Error Envelope

All error responses use an OpenAI-style envelope:

{
"error": {
"message": "Human-readable explanation.",
"type": "invalid_request_error",
"param": null,
"code": null
}
}

error.message is always a plain string. error.type is one of invalid_request_error, authentication_error, rate_limit_error, or server_error. Note that 403 Forbidden responses use authentication_error (same as 401), and 404 Not Found responses use invalid_request_error — there are no separate permission_error or not_found_error types. error.param is always present (null unless a specific parameter is at fault). error.code is a stable machine-readable code where applicable (e.g. too_many_requests for 429, null otherwise).

Common 4xx Errors

400 Bad Request — empty messages array

/v1/chat/completions and /v1/completions reject requests with an empty messages array (messages: []) or messages with unknown roles. Valid roles are user, system, assistant, tool, and function.

{
"error": {
"message": "'messages' field is required and must contain at least one message",
"type": "invalid_request_error",
"param": null,
"code": null
}
}

This validation runs before any token is consumed, so a malformed request does not bill against your quota.

402 Payment Required — billing not configured

Returned only when the requested model has no pricing set up. The message reads "Model <model-name> is not available for billing. Please contact support." (with the requested model name interpolated) and indicates a configuration problem on the platform side, not a transient issue. Contact your administrator.

503 Service Unavailable — billing service unavailable

Transient billing-service hiccups (network blips, brief 5xx from the usage service) now surface as a 503 with "Billing service unavailable. Inference temporarily disabled for safety.". Retry with exponential backoff — the same request usually succeeds within a few seconds. This used to be misreported as a 402 "billing problem"; it is now distinguished from a real configuration issue.

Comprehensive Error Handler

Python

from openai import (
OpenAI,
APIError,
APIConnectionError,
RateLimitError,
AuthenticationError,
BadRequestError,
NotFoundError,
PermissionDeniedError,
UnprocessableEntityError
)
import time
import logging

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

class APIClient:
def __init__(self, api_key, base_url):
self.client = OpenAI(api_key=api_key, base_url=base_url)
self.max_retries = 3

def chat(self, messages, **kwargs):
for attempt in range(self.max_retries):
try:
return self.client.chat.completions.create(
model=kwargs.get('model', 'meta-llama/Llama-3.1-8B-Instruct'),
messages=messages,
**kwargs
)

except AuthenticationError as e:
logger.error(f"Authentication failed: {e.message}")
raise # Don't retry auth errors

except PermissionDeniedError as e:
logger.error(f"Permission denied: {e.message}")
raise # Don't retry permission errors

except NotFoundError as e:
logger.error(f"Resource not found: {e.message}")
raise # Don't retry 404s

except BadRequestError as e:
logger.error(f"Bad request: {e.message}")
raise # Don't retry bad requests

except UnprocessableEntityError as e:
logger.error(f"Validation error: {e.message}")
raise # Don't retry validation errors

except RateLimitError as e:
if attempt == self.max_retries - 1:
raise
wait = 2 ** attempt
logger.warning(f"Rate limited. Waiting {wait}s...")
time.sleep(wait)

except APIConnectionError as e:
if attempt == self.max_retries - 1:
raise
wait = 2 ** attempt
logger.warning(f"Connection error. Retrying in {wait}s...")
time.sleep(wait)

except APIError as e:
if attempt == self.max_retries - 1:
raise
if e.status_code >= 500:
wait = 2 ** attempt
logger.warning(f"Server error {e.status_code}. Retrying in {wait}s...")
time.sleep(wait)
else:
raise

Node.js

import OpenAI from 'openai';

class APIClient {
constructor(apiKey, baseURL) {
this.client = new OpenAI({ apiKey, baseURL });
this.maxRetries = 3;
}

async chat(messages, options = {}) {
for (let attempt = 0; attempt < this.maxRetries; attempt++) {
try {
return await this.client.chat.completions.create({
model: options.model || 'meta-llama/Llama-3.1-8B-Instruct',
messages,
...options
});

} catch (error) {
// Non-retryable errors
if (error instanceof OpenAI.AuthenticationError) {
console.error('Authentication failed:', error.message);
throw error;
}

if (error instanceof OpenAI.PermissionDeniedError) {
console.error('Permission denied:', error.message);
throw error;
}

if (error instanceof OpenAI.NotFoundError) {
console.error('Not found:', error.message);
throw error;
}

if (error instanceof OpenAI.BadRequestError) {
console.error('Bad request:', error.message);
throw error;
}

// Retryable errors
if (error instanceof OpenAI.RateLimitError) {
if (attempt === this.maxRetries - 1) throw error;
const wait = Math.pow(2, attempt) * 1000;
console.warn(`Rate limited. Waiting ${wait}ms...`);
await this.sleep(wait);
continue;
}

if (error instanceof OpenAI.APIConnectionError) {
if (attempt === this.maxRetries - 1) throw error;
const wait = Math.pow(2, attempt) * 1000;
console.warn(`Connection error. Retrying in ${wait}ms...`);
await this.sleep(wait);
continue;
}

if (error instanceof OpenAI.APIError && error.status >= 500) {
if (attempt === this.maxRetries - 1) throw error;
const wait = Math.pow(2, attempt) * 1000;
console.warn(`Server error. Retrying in ${wait}ms...`);
await this.sleep(wait);
continue;
}

throw error;
}
}
}

sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
}

Error Response Parsing

def parse_error(error):
"""Extract useful information from API error."""
return {
'status_code': getattr(error, 'status_code', None),
'message': getattr(error, 'message', str(error)),
'type': type(error).__name__,
'code': getattr(error, 'code', None),
'param': getattr(error, 'param', None)
}

try:
response = client.chat.completions.create(...)
except APIError as e:
error_info = parse_error(e)
logger.error(f"API Error: {error_info}")

User-Friendly Error Messages

ERROR_MESSAGES = {
'authentication_error': 'Invalid API key. Please check your credentials.',
'rate_limit_error': 'Too many requests. Please try again in a moment.',
'model_not_found': 'The requested model is not available.',
'context_length_exceeded': 'Your message is too long. Please shorten it.',
'server_error': 'Our servers are experiencing issues. Please try again later.'
}

def get_user_message(error):
"""Convert technical error to user-friendly message."""
error_type = getattr(error, 'type', type(error).__name__.lower())

if 'rate_limit' in error_type:
return ERROR_MESSAGES['rate_limit_error']
elif 'authentication' in error_type:
return ERROR_MESSAGES['authentication_error']
elif 'not_found' in error_type:
return ERROR_MESSAGES['model_not_found']
elif 'context_length' in str(error).lower():
return ERROR_MESSAGES['context_length_exceeded']
elif getattr(error, 'status_code', 0) >= 500:
return ERROR_MESSAGES['server_error']
else:
return 'An error occurred. Please try again.'

Circuit Breaker Pattern

from datetime import datetime, timedelta
from enum import Enum

class CircuitState(Enum):
CLOSED = 'closed'
OPEN = 'open'
HALF_OPEN = 'half_open'

class CircuitBreaker:
def __init__(self, failure_threshold=5, reset_timeout=60):
self.failure_threshold = failure_threshold
self.reset_timeout = reset_timeout
self.failures = 0
self.state = CircuitState.CLOSED
self.last_failure_time = None

def record_failure(self):
self.failures += 1
self.last_failure_time = datetime.now()

if self.failures >= self.failure_threshold:
self.state = CircuitState.OPEN

def record_success(self):
self.failures = 0
self.state = CircuitState.CLOSED

def can_execute(self):
if self.state == CircuitState.CLOSED:
return True

if self.state == CircuitState.OPEN:
if datetime.now() - self.last_failure_time > timedelta(seconds=self.reset_timeout):
self.state = CircuitState.HALF_OPEN
return True
return False

return True # HALF_OPEN allows one request

# Usage
breaker = CircuitBreaker()

def make_request():
if not breaker.can_execute():
raise Exception("Circuit breaker is open")

try:
response = client.chat.completions.create(...)
breaker.record_success()
return response
except Exception as e:
breaker.record_failure()
raise

Logging Best Practices

import logging
import json

def log_api_error(error, request_data=None):
"""Log API error with context."""
log_data = {
'error_type': type(error).__name__,
'status_code': getattr(error, 'status_code', None),
'message': str(error),
'model': request_data.get('model') if request_data else None,
'timestamp': datetime.now().isoformat()
}

# Don't log sensitive data
if request_data:
log_data['message_count'] = len(request_data.get('messages', []))

logger.error(json.dumps(log_data))

Best Practices Summary

  1. Categorize errors: Retryable vs. non-retryable
  2. Use exponential backoff: For retryable errors
  3. Set retry limits: Prevent infinite loops
  4. Log comprehensively: Include context, exclude secrets
  5. Show user-friendly messages: Translate technical errors
  6. Implement circuit breakers: For cascading failure prevention
  7. Monitor error rates: Alert on anomalies