Error Handling
此内容尚不支持你的语言。
Error handling in Claude Code is sophisticated because the tool must gracefully handle a wide spectrum of API failures — from transient rate limits to authentication errors to complete API outages. The system is centered around withRetry.ts and errors.ts in src/services/api/.
Error Handling Architecture
Section titled “Error Handling Architecture”graph TB subgraph "API Call" CALL["claude.ts → API request"] end
subgraph "withRetry.ts" RETRY["Retry loop (max 10 attempts)"] CLASS["Error classification"] BACKOFF["Exponential backoff + jitter"] FASTMODE["Fast mode fallback"] FALLBACK["Model fallback (529)"] PERSIST["Persistent retry (unattended)"] end
subgraph "errors.ts" MSG["User-facing error messages"] CLASSIFY["Error type classification"] ANALYTICS["Analytics tagging"] end
subgraph "Consumer" UI["REPL UI display"] SDK_OUT["SDK error messages"] end
CALL --> RETRY RETRY -->|Retryable| BACKOFF RETRY -->|Fast mode| FASTMODE RETRY -->|Repeated 529| FALLBACK RETRY -->|Unattended 429/529| PERSIST RETRY -->|Non-retryable| CLASS CLASS --> MSG MSG --> UI MSG --> SDK_OUT BACKOFF --> RETRY FASTMODE --> RETRYThe withRetry Pattern
Section titled “The withRetry Pattern”The withRetry() function in src/services/api/withRetry.ts is an async generator that wraps API calls with retry logic:
export async function* withRetry<T>( getClient: () => Promise<Anthropic>, operation: (client: Anthropic, attempt: number, context: RetryContext) => Promise<T>, options: RetryOptions,): AsyncGenerator<SystemAPIErrorMessage, T> { const maxRetries = getMaxRetries(options) // Default: 10 const retryContext: RetryContext = { model: options.model, thinkingConfig: options.thinkingConfig, ...(isFastModeEnabled() && { fastMode: options.fastMode }), }
let client: Anthropic | null = null let consecutive529Errors = options.initialConsecutive529Errors ?? 0
for (let attempt = 1; attempt <= maxRetries + 1; attempt++) { if (options.signal?.aborted) throw new APIUserAbortError()
try { if (client === null || /* auth error on last attempt */) { client = await getClient() } return await operation(client, attempt, retryContext) } catch (error) { // Classification and retry logic... } } throw new CannotRetryError(lastError, retryContext)}Key design: withRetry yields SystemAPIErrorMessage during waits (for UI display) and returns the final result via the generator’s return value.
HTTP Error Differentiation
Section titled “HTTP Error Differentiation”Claude Code classifies errors by HTTP status code and takes different actions:
429 — Rate Limit
Section titled “429 — Rate Limit”// Rate limit handling depends on subscription typeif (error.status === 429) { // ClaudeAI subscribers (Max/Pro): don't retry (wait could be hours) // Enterprise subscribers: retry (typically PAYG, short limits) // API key users: retry with backoff return !isClaudeAISubscriber() || isEnterpriseSubscriber()}Rate limit responses include headers that guide behavior:
// Headers checked for rate limiting'anthropic-ratelimit-unified-representative-claim' // 'five_hour' | 'seven_day''anthropic-ratelimit-unified-overage-status' // 'allowed' | 'rejected''anthropic-ratelimit-unified-reset' // Unix timestamp'anthropic-ratelimit-unified-overage-disabled-reason' // Why extra usage is blocked529 — Server Overloaded
Section titled “529 — Server Overloaded”export function is529Error(error: unknown): boolean { if (!(error instanceof APIError)) return false return ( error.status === 529 || // SDK sometimes fails to pass 529 status during streaming (error.message?.includes('"type":"overloaded_error"') ?? false) )}529 errors have special query source handling — only foreground queries retry:
// Only foreground sources retry on 529 to avoid amplificationconst FOREGROUND_529_RETRY_SOURCES = new Set<QuerySource>([ 'repl_main_thread', 'sdk', 'agent:custom', 'agent:default', 'compact', 'auto_mode', // Background sources (summaries, titles, classifiers) bail immediately])
function shouldRetry529(querySource: QuerySource | undefined): boolean { return querySource === undefined || FOREGROUND_529_RETRY_SOURCES.has(querySource)}Design rationale: During capacity cascades, each retry amplifies load 3-10×. Background queries (title generation, suggestions) that the user never sees should fail silently rather than making the cascade worse.
500+ — Server Errors
Section titled “500+ — Server Errors”// Always retry internal server errorsif (error.status && error.status >= 500) return true401 — Authentication Error
Section titled “401 — Authentication Error”if (error.status === 401) { // Clear cached API key and retry clearApiKeyHelperCache()
// For OAuth: force token refresh if (lastError instanceof APIError && lastError.status === 401) { const failedAccessToken = getClaudeAIOAuthTokens()?.accessToken if (failedAccessToken) { await handleOAuth401Error(failedAccessToken) } }
return true // Retry with refreshed credentials}Connection Errors (ECONNRESET/EPIPE)
Section titled “Connection Errors (ECONNRESET/EPIPE)”function isStaleConnectionError(error: unknown): boolean { if (!(error instanceof APIConnectionError)) return false const details = extractConnectionErrorDetails(error) return details?.code === 'ECONNRESET' || details?.code === 'EPIPE'}
// On stale connection: disable keep-alive and reconnectif (isStaleConnection) { disableKeepAlive() client = await getClient() // Force new connection}Exponential Backoff with Jitter
Section titled “Exponential Backoff with Jitter”export const BASE_DELAY_MS = 500
export function getRetryDelay( attempt: number, retryAfterHeader?: string | null, maxDelayMs = 32000,): number { // Honor server's Retry-After header if present if (retryAfterHeader) { const seconds = parseInt(retryAfterHeader, 10) if (!isNaN(seconds)) return seconds * 1000 }
// Exponential backoff: 500ms, 1s, 2s, 4s, 8s, 16s, 32s (capped) const baseDelay = Math.min( BASE_DELAY_MS * Math.pow(2, attempt - 1), maxDelayMs, ) // Add 25% jitter to prevent thundering herd const jitter = Math.random() * 0.25 * baseDelay return baseDelay + jitter}The retry delay sequence for default settings:
| Attempt | Base Delay | With Jitter (approx) |
|---|---|---|
| 1 | 500ms | 500-625ms |
| 2 | 1,000ms | 1,000-1,250ms |
| 3 | 2,000ms | 2,000-2,500ms |
| 4 | 4,000ms | 4,000-5,000ms |
| 5 | 8,000ms | 8,000-10,000ms |
| 6 | 16,000ms | 16,000-20,000ms |
| 7+ | 32,000ms | 32,000-40,000ms |
Fast Mode → Normal Mode Fallback
Section titled “Fast Mode → Normal Mode Fallback”When fast mode is active, 429/529 errors trigger a fallback mechanism:
const SHORT_RETRY_THRESHOLD_MS = 20 * 1000 // 20 secondsconst MIN_COOLDOWN_MS = 10 * 60 * 1000 // 10 minutesconst DEFAULT_FAST_MODE_FALLBACK_HOLD_MS = 30 * 60 * 1000 // 30 minutes
if (wasFastModeActive && (error.status === 429 || is529Error(error))) { const retryAfterMs = getRetryAfterMs(error)
if (retryAfterMs !== null && retryAfterMs < SHORT_RETRY_THRESHOLD_MS) { // Short retry-after (<20s): wait and retry with fast mode still active // Preserves prompt cache (same model name) await sleep(retryAfterMs, options.signal) continue }
// Long or unknown retry-after: enter cooldown const cooldownMs = Math.max( retryAfterMs ?? DEFAULT_FAST_MODE_FALLBACK_HOLD_MS, MIN_COOLDOWN_MS, ) triggerFastModeCooldown(Date.now() + cooldownMs, cooldownReason) retryContext.fastMode = false continue}graph TB A["429/529 in Fast Mode"] --> B{Retry-After < 20s?} B -->|Yes| C["Wait & retry<br/>(keep fast mode)"] B -->|No| D["Enter cooldown<br/>(switch to normal)"] D --> E["Cooldown for<br/>max(retryAfter, 10min)"] E --> F["Retry with<br/>normal mode model"]
A --> G{Overage disabled?} G -->|Yes| H["Permanently disable<br/>fast mode"]Model Fallback (529 → Sonnet)
Section titled “Model Fallback (529 → Sonnet)”After 3 consecutive 529 errors, Claude Code can fall back from the primary model to a fallback:
const MAX_529_RETRIES = 3
if (is529Error(error)) { consecutive529Errors++ if (consecutive529Errors >= MAX_529_RETRIES) { if (options.fallbackModel) { // Throw special error — caller catches and retries with fallback model throw new FallbackTriggeredError(options.model, options.fallbackModel) }
// External users with no fallback: give up if (process.env.USER_TYPE === 'external') { throw new CannotRetryError( new Error(REPEATED_529_ERROR_MESSAGE), retryContext, ) } }}The FallbackTriggeredError is caught by query.ts, which restarts the API call with the fallback model.
Persistent Retry for Unattended Sessions
Section titled “Persistent Retry for Unattended Sessions”For unattended (headless) sessions, Claude Code supports persistent retry — indefinite retries with keep-alive heartbeats:
const PERSISTENT_MAX_BACKOFF_MS = 5 * 60 * 1000 // 5 minutes max backoffconst PERSISTENT_RESET_CAP_MS = 6 * 60 * 60 * 1000 // 6 hours max waitconst HEARTBEAT_INTERVAL_MS = 30_000 // 30 second heartbeats
function isPersistentRetryEnabled(): boolean { return isEnvTruthy(process.env.CLAUDE_CODE_UNATTENDED_RETRY)}When persistent retry is active:
if (persistent) { // Chunk long sleeps to emit heartbeats let remaining = delayMs while (remaining > 0) { if (options.signal?.aborted) throw new APIUserAbortError()
// Yield status message as heartbeat yield createSystemAPIErrorMessage(error, remaining, reportedAttempt, maxRetries)
const chunk = Math.min(remaining, HEARTBEAT_INTERVAL_MS) await sleep(chunk, options.signal) remaining -= chunk }
// Clamp attempt counter — the for-loop never terminates if (attempt >= maxRetries) attempt = maxRetries}Why heartbeats? The host environment (CI system, orchestrator) may kill idle sessions. Each yielded SystemAPIErrorMessage produces stdout activity via QueryEngine, keeping the session alive.
For 429 errors with rate limit reset headers, persistent retry honors the exact reset time:
function getRateLimitResetDelayMs(error: APIError): number | null { const resetHeader = error.headers?.get?.('anthropic-ratelimit-unified-reset') if (!resetHeader) return null const resetUnixSec = Number(resetHeader) const delayMs = resetUnixSec * 1000 - Date.now() return Math.min(delayMs, PERSISTENT_RESET_CAP_MS)}Error Classification for Analytics
Section titled “Error Classification for Analytics”The classifyAPIError() function in errors.ts maps errors to standardized tags:
export function classifyAPIError(error: unknown): string { if (error instanceof APIConnectionTimeoutError) return 'api_timeout' if (error.message.includes(REPEATED_529_ERROR_MESSAGE)) return 'repeated_529' if (error instanceof APIError && error.status === 429) return 'rate_limit' if (error instanceof APIError && error.status === 529) return 'server_overload' if (error.message.includes('prompt is too long')) return 'prompt_too_long' if (error.message.includes('x-api-key')) return 'invalid_api_key' if (error instanceof APIError && error.status >= 500) return 'server_error' if (error instanceof APIConnectionError) { const details = extractConnectionErrorDetails(error) if (details?.isSSLError) return 'ssl_cert_error' return 'connection_error' } return 'unknown'}Full classification taxonomy:
| Error Type | HTTP Status | Description |
|---|---|---|
api_timeout | — | Connection timeout |
rate_limit | 429 | Rate limited |
server_overload | 529 | API overloaded |
repeated_529 | 529 | 3+ consecutive 529s |
prompt_too_long | 400 | Input exceeds context window |
pdf_too_large | 400 | PDF exceeds page limit |
image_too_large | 400 | Image exceeds size limit |
tool_use_mismatch | 400 | tool_use/tool_result pairing error |
invalid_model | 400 | Model name not recognized |
credit_balance_low | — | Insufficient credits |
invalid_api_key | 401 | Bad API key |
token_revoked | 403 | OAuth token revoked |
auth_error | 401/403 | Generic auth failure |
server_error | 500+ | Internal server error |
connection_error | — | Network connectivity |
ssl_cert_error | — | SSL/TLS certificate issue |
User-Facing Error Messages
Section titled “User-Facing Error Messages”The getAssistantMessageFromError() function translates API errors into user-friendly messages:
export function getAssistantMessageFromError( error: unknown, model: string,): AssistantMessage { // Timeout → "Request timed out" // Image too large → "Image was too large. Try resizing..." // Prompt too long → "Prompt is too long" // 429 with headers → Specific rate limit message with reset time // 401 → "Please run /login" or "Invalid API key" // 403 OAuth revoked → "OAuth token revoked · Please run /login" // 529 → "Repeated 529 Overloaded errors" // Generic → "API Error: {message}"}Context-Aware Messages
Section titled “Context-Aware Messages”Error messages adapt to the execution context:
// Interactive mode gets UI hints'PDF too large. Double press esc to go back and try again'
// SDK/headless mode gets actionable advice'PDF too large. Try reading the file a different way (e.g., extract text with pdftotext).'Error Handling Flow Summary
Section titled “Error Handling Flow Summary”flowchart TD ERR["API Error"] --> IS_ABORT{Aborted?} IS_ABORT -->|Yes| THROW_ABORT["Throw APIUserAbortError"] IS_ABORT -->|No| IS_FAST{Fast mode active?}
IS_FAST -->|Yes| FAST_429{429/529?} FAST_429 -->|Short retry| FAST_RETRY["Wait, keep fast mode"] FAST_429 -->|Long retry| FAST_COOL["Cooldown, switch normal"]
IS_FAST -->|No| IS_529{529?} IS_529 -->|Yes| FG{Foreground query?} FG -->|No| DROP["Drop immediately<br/>(no amplification)"] FG -->|Yes| COUNT{3+ consecutive?} COUNT -->|Yes| FALLBACK["FallbackTriggeredError<br/>(switch model)"] COUNT -->|No| RETRY_529["Retry with backoff"]
IS_529 -->|No| IS_429{429?} IS_429 -->|Yes| SUB{Subscriber type?} SUB -->|ClaudeAI Max/Pro| NO_RETRY["Show rate limit message"] SUB -->|Enterprise/API| RETRY_429["Retry with backoff"]
IS_429 -->|No| IS_AUTH{401/403?} IS_AUTH -->|Yes| REFRESH["Refresh credentials, retry"] IS_AUTH -->|No| IS_5XX{5xx?} IS_5XX -->|Yes| RETRY_5XX["Retry with backoff"] IS_5XX -->|No| CANNOT_RETRY["CannotRetryError"]