Appendix C: Error Codes and Troubleshooting
Appendix C: Error Codes and Troubleshooting
Section titled “Appendix C: Error Codes and Troubleshooting”Complete guide to Claude API errors, debugging strategies, and solutions. Use this as your first stop when encountering issues.
Table of Contents
Section titled “Table of Contents”- Error Response Format
- ClaudePhp Errors (4xx)
- Server Errors (5xx)
- SDK-Specific Errors
- Common Issues
- Debugging Strategies
- Best Practices
Error Response Format
Section titled “Error Response Format”Standard Error Structure
Section titled “Standard Error Structure”{ "type": "error", "error": { "type": "invalid_request_error", "message": "max_tokens: Field required" }}PHP SDK Error Object
Section titled “PHP SDK Error Object”use ClaudePhp\ClaudePhp;
$client = new ClaudePhp( apiKey: getenv('ANTHROPIC_API_KEY'));
try { $response = $client->messages()->create([...]);} catch (\ClaudePhp\Exceptions\ErrorException $e) { echo $e->getMessage(); // Error message echo $e->getErrorType(); // Error type code echo $e->getStatusCode(); // HTTP status code}ClaudePhp Errors (4xx)
Section titled “ClaudePhp Errors (4xx)”Errors caused by invalid requests from your application.
400 - invalid_request_error
Section titled “400 - invalid_request_error”Cause: Request is malformed or missing required parameters.
Common Examples:
// Missing required field[ "type" => "error", "error" => [ "type" => "invalid_request_error", "message" => "max_tokens: Field required" ]]Solutions:
// ❌ Wrong - missing max_tokens$response = $client->messages()->create([ 'model' => 'claude-sonnet-4-5-20250929', 'messages' => [ ['role' => 'user', 'content' => 'Hello'] ]]);
// ✅ Correct$response = $client->messages()->create([ 'model' => 'claude-sonnet-4-5-20250929', 'max_tokens' => 1024, // Required! 'messages' => [ ['role' => 'user', 'content' => 'Hello'] ]]);Common Validation Errors:
| Message | Cause | Fix |
|---|---|---|
max_tokens: Field required | Missing max_tokens | Add max_tokens parameter |
messages: Field required | Missing messages array | Add messages parameter |
messages: Array must not be empty | Empty messages array | Add at least one message |
messages.0.role: Must be 'user' or 'assistant' | Invalid role | Use ‘user’ or ‘assistant’ |
messages.0: First message must have role 'user' | Wrong first role | Start with user message |
messages: Roles must alternate | Non-alternating roles | Alternate user/assistant |
model: Invalid model identifier | Wrong model name | Use valid model ID |
temperature: Must be between 0 and 1 | Invalid temperature | Use 0.0-1.0 range |
max_tokens: Must be between 1 and 4096 | Invalid max_tokens | Use 1-4096 range |
Validation Helper:
class ClaudeRequestValidator{ public static function validate(array $params): array { $errors = [];
// Required fields if (!isset($params['model'])) { $errors[] = 'model is required'; }
if (!isset($params['max_tokens'])) { $errors[] = 'max_tokens is required'; } elseif ($params['max_tokens'] < 1 || $params['max_tokens'] > 4096) { $errors[] = 'max_tokens must be between 1 and 4096'; }
if (!isset($params['messages'])) { $errors[] = 'messages is required'; } elseif (empty($params['messages'])) { $errors[] = 'messages cannot be empty'; } elseif ($params['messages'][0]['role'] !== 'user') { $errors[] = 'First message must have role "user"'; }
// Validate temperature if (isset($params['temperature'])) { if ($params['temperature'] < 0 || $params['temperature'] > 1) { $errors[] = 'temperature must be between 0 and 1'; } }
return $errors; }}
// Usage$errors = ClaudeRequestValidator::validate($params);if (!empty($errors)) { throw new InvalidArgumentException(implode(', ', $errors));}401 - authentication_error
Section titled “401 - authentication_error”Cause: Invalid or missing API key.
Error Response:
{ "type": "error", "error": { "type": "authentication_error", "message": "invalid x-api-key" }}Common Causes:
- Missing API key
- Invalid API key format
- Expired API key
- Wrong environment variable
Solutions:
use ClaudePhp\ClaudePhp;
// Check if API key is set$apiKey = getenv('ANTHROPIC_API_KEY');if (!$apiKey) { throw new RuntimeException('ANTHROPIC_API_KEY not set');}
// Verify API key format (should start with 'sk-ant-')if (!str_starts_with($apiKey, 'sk-ant-')) { throw new RuntimeException('Invalid API key format');}
// Initialize client$client = new ClaudePhp( apiKey: $apiKey);Debug Checklist:
- API key is set in environment variables
- Using correct .env file (local vs production)
- API key starts with
sk-ant- - No extra whitespace in API key
- API key is still active (check console.anthropic.com)
- Using correct header (
x-api-key, notAuthorization)
403 - permission_error
Section titled “403 - permission_error”Cause: API key doesn’t have permission to access the resource or feature.
Error Response:
{ "type": "error", "error": { "type": "permission_error", "message": "Your API key does not have permission to use the specified model" }}Common Causes:
- Model not available in your tier
- Beta features without beta header
- Account suspended
Solutions:
use ClaudePhp\ClaudePhp;
// Check model availability$availableModels = [ 'claude-opus-4-20250514', 'claude-sonnet-4-5-20250929', 'claude-haiku-4-5-20251001'];
if (!in_array($model, $availableModels)) { throw new RuntimeException("Model {$model} not available");}
// For beta features, add beta header$client = new ClaudePhp( apiKey: getenv('ANTHROPIC_API_KEY'), betaFeatures: ['prompt-caching-2024-07-31']);404 - not_found_error
Section titled “404 - not_found_error”Cause: Requested resource doesn’t exist.
Error Response:
{ "type": "error", "error": { "type": "not_found_error", "message": "Not found" }}Common Causes:
- Wrong API endpoint URL
- Typo in endpoint path
Solution:
use ClaudePhp\ClaudePhp;
// ❌ Wrong$url = 'https://api.anthropic.com/v1/message'; // Missing 's'
// ✅ Correct$url = 'https://api.anthropic.com/v1/messages';
// When using SDK, this is handled automatically$client = new ClaudePhp( apiKey: getenv('ANTHROPIC_API_KEY'));429 - rate_limit_error
Section titled “429 - rate_limit_error”Cause: Too many requests in a time window.
Error Response:
{ "type": "error", "error": { "type": "rate_limit_error", "message": "Rate limit exceeded" }}Rate Limit Types:
- Requests per minute (RPM)
- Tokens per minute (TPM)
- Tokens per day (TPD)
Solutions:
use ClaudePhp\Exceptions\ErrorException;
class RateLimiter{ private int $maxRetries = 5; private int $baseDelay = 1000000; // 1 second in microseconds
public function makeRequest(callable $request, int $attempt = 0) { try { return $request(); } catch (ErrorException $e) { if ($e->getErrorType() === 'rate_limit_error' && $attempt < $this->maxRetries) { // Exponential backoff: 1s, 2s, 4s, 8s, 16s $delay = min($this->baseDelay * pow(2, $attempt), 32000000); // Max 32s
// Add jitter to prevent thundering herd $jitter = random_int(0, 100000); // 0-100ms usleep($delay + $jitter);
return $this->makeRequest($request, $attempt + 1); }
throw $e; } }}
// Usage$rateLimiter = new RateLimiter();
$response = $rateLimiter->makeRequest(function() use ($client, $params) { return $client->messages()->create($params);});Advanced Rate Limiting with Redis:
class RedisRateLimiter{ private Redis $redis; private int $requestsPerMinute;
public function __construct(Redis $redis, int $requestsPerMinute = 50) { $this->redis = $redis; $this->requestsPerMinute = $requestsPerMinute; }
public function allowRequest(string $identifier): bool { $key = "rate_limit:{$identifier}:" . floor(time() / 60); $current = $this->redis->incr($key);
if ($current === 1) { $this->redis->expire($key, 60); }
return $current <= $this->requestsPerMinute; }
public function waitForSlot(string $identifier): void { while (!$this->allowRequest($identifier)) { usleep(100000); // Wait 100ms } }}
// Usageuse ClaudePhp\ClaudePhp;
$client = new ClaudePhp( apiKey: getenv('ANTHROPIC_API_KEY'));
$limiter = new RedisRateLimiter($redis, 50);$limiter->waitForSlot('user_123');
$response = $client->messages()->create($params);Check Rate Limit Headers:
// After making a request, check remaining quotaif (method_exists($response, 'headers')) { $remaining = $response->headers['anthropic-ratelimit-requests-remaining'] ?? null; $reset = $response->headers['anthropic-ratelimit-requests-reset'] ?? null;
if ($remaining !== null && $remaining < 10) { Log::warning("Rate limit running low: {$remaining} requests remaining"); }}Server Errors (5xx)
Section titled “Server Errors (5xx)”Errors on Anthropic’s side. Usually transient.
500 - api_error
Section titled “500 - api_error”Cause: Internal server error on Anthropic’s side.
Error Response:
{ "type": "error", "error": { "type": "api_error", "message": "Internal server error" }}Solution: Retry with exponential backoff.
use ClaudePhp\Exceptions\ErrorException;
class ApiErrorHandler{ public function makeRequestWithRetry(callable $request, int $maxAttempts = 3): mixed { $attempt = 0;
while ($attempt < $maxAttempts) { try { return $request(); } catch (ErrorException $e) { if ($e->getErrorType() === 'api_error' && $attempt < $maxAttempts - 1) { $attempt++; $waitTime = min(pow(2, $attempt) * 1000000, 32000000); // Max 32s usleep($waitTime); continue; } throw $e; } } }}529 - overloaded_error
Section titled “529 - overloaded_error”Cause: Anthropic’s API is temporarily overloaded.
Error Response:
{ "type": "error", "error": { "type": "overloaded_error", "message": "Overloaded" }}Solution: Retry with longer delays.
use ClaudePhp\Exceptions\ErrorException;
class OverloadHandler{ public function makeRequest(callable $request, int $maxRetries = 5): mixed { $attempt = 0;
while ($attempt < $maxRetries) { try { return $request(); } catch (ErrorException $e) { if ($e->getErrorType() === 'overloaded_error') { $attempt++;
if ($attempt >= $maxRetries) { throw new RuntimeException('API overloaded after max retries', 0, $e); }
// Longer delays for overload: 5s, 10s, 20s, 40s, 80s $delay = min(5 * pow(2, $attempt) * 1000000, 80000000); usleep($delay); continue; } throw $e; } } }}SDK-Specific Errors
Section titled “SDK-Specific Errors”Connection Timeout
Section titled “Connection Timeout”Cause: Request took too long to complete.
use ClaudePhp\ClaudePhp;
// Set custom timeout$client = new ClaudePhp( apiKey: getenv('ANTHROPIC_API_KEY'), httpClientOptions: [ 'timeout' => 120, // 2 minutes 'connect_timeout' => 10 // 10 seconds to connect ]);SSL Certificate Errors
Section titled “SSL Certificate Errors”Cause: SSL verification issues.
use ClaudePhp\ClaudePhp;
// Development only - DO NOT use in production$client = new ClaudePhp( apiKey: getenv('ANTHROPIC_API_KEY'), httpClientOptions: [ 'verify' => false // Disable SSL verification ]);
// Production - specify CA bundle$client = new ClaudePhp( apiKey: getenv('ANTHROPIC_API_KEY'), httpClientOptions: [ 'verify' => '/path/to/cacert.pem' ]);Common Issues
Section titled “Common Issues”Issue: Empty or Null Response
Section titled “Issue: Empty or Null Response”Symptoms:
- Response object is null
- No content in response
- Empty message
Causes & Solutions:
// 1. Check stop_reasonif ($response->stop_reason === 'max_tokens') { // Response was truncated - increase max_tokens $params['max_tokens'] = 2048;}
// 2. Verify response structureif (empty($response->content)) { Log::error('Empty response content', ['response' => $response]);}
// 3. Check for tool_use stopif ($response->stop_reason === 'tool_use') { // Claude wants to use a tool - handle it foreach ($response->content as $block) { if ($block->type === 'tool_use') { // Execute tool and continue conversation } }}Issue: Unexpected Response Format
Section titled “Issue: Unexpected Response Format”Symptoms:
- JSON parsing errors
- Missing expected fields
- Wrong data types
Solution:
class ResponseValidator{ public static function validateMessageResponse($response): void { if (!isset($response->id)) { throw new RuntimeException('Response missing id field'); }
if (!isset($response->content) || !is_array($response->content)) { throw new RuntimeException('Response missing or invalid content'); }
if (empty($response->content)) { throw new RuntimeException('Response content is empty'); }
if (!isset($response->usage)) { throw new RuntimeException('Response missing usage field'); } }
public static function extractText($response): string { self::validateMessageResponse($response);
$textBlocks = array_filter( $response->content, fn($block) => $block->type === 'text' );
if (empty($textBlocks)) { throw new RuntimeException('No text blocks in response'); }
return implode("\n", array_map( fn($block) => $block->text, $textBlocks )); }}
// Usagetry { $response = $client->messages()->create($params); ResponseValidator::validateMessageResponse($response); $text = ResponseValidator::extractText($response);} catch (RuntimeException $e) { Log::error('Invalid response', ['error' => $e->getMessage()]);}Issue: Conversation Context Lost
Section titled “Issue: Conversation Context Lost”Symptoms:
- Claude doesn’t remember previous messages
- Responses lack context
- Conversation feels disjointed
Solution:
class ConversationManager{ private array $messages = []; private int $maxTokens = 100000; // Keep under 200K limit
public function addMessage(string $role, string $content): void { $this->messages[] = [ 'role' => $role, 'content' => $content ];
$this->pruneIfNeeded(); }
public function getMessages(): array { return $this->messages; }
private function pruneIfNeeded(): void { // Rough token estimation: 1 token ≈ 4 characters $totalChars = array_sum(array_map( fn($msg) => strlen($msg['content']), $this->messages ));
$estimatedTokens = $totalChars / 4;
if ($estimatedTokens > $this->maxTokens) { // Keep system message and recent messages $system = array_shift($this->messages); // Remove first (system) $this->messages = array_slice($this->messages, -20); // Keep last 20 array_unshift($this->messages, $system); // Add system back } }}
// Usage$conversation = new ConversationManager();$conversation->addMessage('user', 'Hello');
$response = $client->messages()->create([ 'model' => 'claude-sonnet-4-5-20250929', 'max_tokens' => 1024, 'messages' => $conversation->getMessages()]);
$conversation->addMessage('assistant', $response->content[0]->text);Issue: Tool Use Not Working
Section titled “Issue: Tool Use Not Working”Symptoms:
- Claude doesn’t use defined tools
- Tool calls malformed
- Tool results not processed
Debug Steps:
// 1. Verify tool definition format$tools = [ [ 'name' => 'get_weather', 'description' => 'Get weather for a city', // Clear description 'input_schema' => [ 'type' => 'object', 'properties' => [ 'city' => [ 'type' => 'string', 'description' => 'City name' // Describe each param ] ], 'required' => ['city'] // Specify required fields ] ]];
// 2. Log tool use attemptsforeach ($response->content as $block) { if ($block->type === 'tool_use') { Log::info('Tool use detected', [ 'tool' => $block->name, 'input' => $block->input, 'id' => $block->id ]); }}
// 3. Ensure tool results are properly formatted$toolResult = [ 'type' => 'tool_result', 'tool_use_id' => $block->id, // Must match tool_use id 'content' => json_encode($result) // String content];Debugging Strategies
Section titled “Debugging Strategies”Enable Debug Logging
Section titled “Enable Debug Logging”class ClaudeDebugger{ private $client; private bool $debugMode;
public function __construct($client, bool $debugMode = false) { $this->client = $client; $this->debugMode = $debugMode; }
public function createMessage(array $params) { if ($this->debugMode) { Log::debug('Claude Request', [ 'model' => $params['model'], 'max_tokens' => $params['max_tokens'], 'message_count' => count($params['messages']), 'has_tools' => isset($params['tools']), 'temperature' => $params['temperature'] ?? 1.0 ]); }
$startTime = microtime(true);
try { $response = $this->client->messages()->create($params);
if ($this->debugMode) { $duration = microtime(true) - $startTime; Log::debug('Claude Response', [ 'duration' => round($duration, 2) . 's', 'stop_reason' => $response->stop_reason, 'input_tokens' => $response->usage->input_tokens, 'output_tokens' => $response->usage->output_tokens, 'cost' => $this->estimateCost($response, $params['model']) ]); }
return $response; } catch (\Exception $e) { if ($this->debugMode) { Log::error('Claude Error', [ 'error' => $e->getMessage(), 'type' => method_exists($e, 'getErrorType') ? $e->getErrorType() : 'unknown', 'params' => $params ]); } throw $e; } }
private function estimateCost($response, string $model): float { $pricing = [ 'claude-opus-4-20250514' => ['input' => 15, 'output' => 75], 'claude-sonnet-4-5-20250929' => ['input' => 3, 'output' => 15], 'claude-haiku-4-5-20251001' => ['input' => 1, 'output' => 5], ];
$rates = $pricing[$model] ?? ['input' => 3, 'output' => 15];
$inputCost = ($response->usage->input_tokens / 1_000_000) * $rates['input']; $outputCost = ($response->usage->output_tokens / 1_000_000) * $rates['output'];
return round($inputCost + $outputCost, 6); }}
// Usage$debugger = new ClaudeDebugger($client, debugMode: true);$response = $debugger->createMessage($params);Request/Response Inspector
Section titled “Request/Response Inspector”class RequestInspector{ public static function inspect(array $params): void { echo "=== REQUEST INSPECTION ===\n"; echo "Model: {$params['model']}\n"; echo "Max Tokens: {$params['max_tokens']}\n"; echo "Messages: " . count($params['messages']) . "\n";
foreach ($params['messages'] as $i => $msg) { $length = is_string($msg['content']) ? strlen($msg['content']) : count($msg['content']); echo " [{$i}] {$msg['role']}: {$length} chars/blocks\n"; }
if (isset($params['tools'])) { echo "Tools: " . count($params['tools']) . "\n"; foreach ($params['tools'] as $tool) { echo " - {$tool['name']}\n"; } }
if (isset($params['temperature'])) { echo "Temperature: {$params['temperature']}\n"; }
echo "========================\n\n"; }
public static function inspectResponse($response): void { echo "=== RESPONSE INSPECTION ===\n"; echo "ID: {$response->id}\n"; echo "Model: {$response->model}\n"; echo "Stop Reason: {$response->stop_reason}\n"; echo "Input Tokens: {$response->usage->input_tokens}\n"; echo "Output Tokens: {$response->usage->output_tokens}\n"; echo "Content Blocks: " . count($response->content) . "\n";
foreach ($response->content as $i => $block) { echo " [{$i}] Type: {$block->type}\n"; if ($block->type === 'text') { echo " Length: " . strlen($block->text) . " chars\n"; } elseif ($block->type === 'tool_use') { echo " Tool: {$block->name}\n"; } }
echo "===========================\n\n"; }}
// UsageRequestInspector::inspect($params);$response = $client->messages()->create($params);RequestInspector::inspectResponse($response);Best Practices
Section titled “Best Practices”Comprehensive Error Handling
Section titled “Comprehensive Error Handling”use ClaudePhp\ClaudePhp;use ClaudePhp\Exceptions\ErrorException;
class ClaudeClient{ private ClaudePhp $client; private LoggerInterface $logger;
public function __construct(string $apiKey, LoggerInterface $logger) { $this->client = new ClaudePhp(apiKey: $apiKey); $this->logger = $logger; }
public function sendMessage(array $params): ?object { try { return $this->client->messages()->create($params);
} catch (ErrorException $e) { return $this->handleApiError($e, $params);
} catch (\GuzzleHttp\Exception\ConnectException $e) { $this->logger->error('Connection failed', [ 'error' => $e->getMessage() ]); throw new RuntimeException('Failed to connect to Claude API', 0, $e);
} catch (\GuzzleHttp\Exception\RequestException $e) { $this->logger->error('Request failed', [ 'error' => $e->getMessage(), 'code' => $e->getCode() ]); throw new RuntimeException('Claude API request failed', 0, $e);
} catch (\Exception $e) { $this->logger->error('Unexpected error', [ 'error' => $e->getMessage(), 'trace' => $e->getTraceAsString() ]); throw $e; } }
private function handleApiError(ErrorException $e, array $params): ?object { $errorType = $e->getErrorType();
$this->logger->warning('Claude API error', [ 'type' => $errorType, 'message' => $e->getMessage(), 'status' => $e->getStatusCode() ]);
return match($errorType) { 'rate_limit_error' => $this->retryWithBackoff($params), 'overloaded_error' => $this->retryWithBackoff($params, maxAttempts: 5), 'api_error' => $this->retryWithBackoff($params, maxAttempts: 3), 'invalid_request_error' => throw new InvalidArgumentException( 'Invalid request: ' . $e->getMessage(), 0, $e ), 'authentication_error' => throw new RuntimeException( 'Authentication failed - check API key', 0, $e ), default => throw $e }; }
private function retryWithBackoff(array $params, int $maxAttempts = 3): ?object { // Implementation from earlier examples // ... }}Circuit Breaker Pattern
Section titled “Circuit Breaker Pattern”use ClaudePhp\ClaudePhp;
class CircuitBreaker{ private const STATE_CLOSED = 'closed'; private const STATE_OPEN = 'open'; private const STATE_HALF_OPEN = 'half_open';
private string $state = self::STATE_CLOSED; private int $failures = 0; private int $threshold = 5; private int $timeout = 60; // seconds private ?int $openedAt = null;
public function execute(callable $callback) { if ($this->state === self::STATE_OPEN) { if (time() - $this->openedAt >= $this->timeout) { $this->state = self::STATE_HALF_OPEN; } else { throw new RuntimeException('Circuit breaker is OPEN'); } }
try { $result = $callback(); $this->onSuccess(); return $result; } catch (\Exception $e) { $this->onFailure(); throw $e; } }
private function onSuccess(): void { $this->failures = 0; $this->state = self::STATE_CLOSED; }
private function onFailure(): void { $this->failures++;
if ($this->failures >= $this->threshold) { $this->state = self::STATE_OPEN; $this->openedAt = time(); } }}
// Usage$client = new ClaudePhp( apiKey: getenv('ANTHROPIC_API_KEY'));
$breaker = new CircuitBreaker();
try { $response = $breaker->execute(fn() => $client->messages()->create($params));} catch (RuntimeException $e) { if ($e->getMessage() === 'Circuit breaker is OPEN') { // Use fallback or cached response $response = $this->getFallbackResponse(); }}Getting Help
Section titled “Getting Help”If you’re still stuck after trying these solutions:
- Check API Status: status.anthropic.com
- Official Docs: docs.claude.com
- Discord Community: discord.gg/anthropic
- GitHub Issues: github.com/anthropics/anthropic-sdk-php
::: tip Quick Navigation
- ← Appendix A: API Reference - Complete API reference
- ← Appendix B: Prompting Patterns - Prompt templates
- ← Appendix C: Error Codes - Troubleshooting guide
- Appendix D: Resources → - Tools and resources
- Back to Series - Return to main series :::
Last updated: November 2024