Error Handling
Best practices for handling API errors gracefully.
Error Categories
| Category | Status | Recovery |
|---|---|---|
| Client errors | 4xx | Fix request |
| Rate limits | 429 | Retry with backoff |
| Server errors | 5xx | Retry |
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
- Categorize errors: Retryable vs. non-retryable
- Use exponential backoff: For retryable errors
- Set retry limits: Prevent infinite loops
- Log comprehensively: Include context, exclude secrets
- Show user-friendly messages: Translate technical errors
- Implement circuit breakers: For cascading failure prevention
- Monitor error rates: Alert on anomalies