Error Handling & Debugging Guide
Comprehensive guide to handling errors gracefully and debugging effectively in Claude Code applications.
Overview
Building robust Claude Code applications requires sophisticated error handling and debugging strategies. This guide covers everything from understanding common errors to implementing production-grade error handling systems.
1. Understanding Common Errors
API-Related Errors
Authentication Errors (401)
try:
client = anthropic.Anthropic() # Missing API key
except anthropic.AuthenticationError as e:
print(f"Authentication failed: {e}")
# Check ANTHROPIC_API_KEY environment variableCommon Causes:
- Missing or invalid API key
- Expired credentials
- Wrong authentication method
Rate Limit Errors (429)
except anthropic.RateLimitError as e:
retry_after = e.response.headers.get('retry-after')
print(f"Rate limited. Retry after {retry_after}s")Handling Strategy:
- Implement exponential backoff
- Monitor usage with
/costcommand - Use token bucket algorithm
Bad Request Errors (400)
except anthropic.BadRequestError as e:
print(f"Invalid request: {e}")
# Check prompt format, model name, parametersClaude Code Specific Errors
Tool Execution Errors
# Common tool errors
- Permission denied for file operations
- Command timeout exceeded
- Invalid file paths
- Network access restrictionsDebug Commands:
# Check permissions
claude /permissions
# Fix configuration issues
claude /doctor
# Review tool settings
claude /configContext Window Errors
# Error: Context length exceeded
except anthropic.BadRequestError as e:
if "context_length_exceeded" in str(e):
# Use /compact or /clear commands
print("Context too large. Consider using /compact")AI-Specific Errors
Output Validation Errors
def validate_ai_output(response):
"""Validate AI-generated content"""
try:
# Check for common issues
if "```" in response and response.count("```") % 2 != 0:
raise ValueError("Unclosed code block")
# Validate JSON responses
if response.startswith("{"):
json.loads(response)
return True
except Exception as e:
logger.error(f"Output validation failed: {e}")
return False2. Debugging Strategies
Systematic Debugging Approach
1. Reproduce the Issue
# Create minimal reproduction
def reproduce_error():
"""Minimal code to reproduce the issue"""
try:
# Isolate the problematic code
result = problematic_function()
except Exception as e:
logger.error(f"Reproduced error: {e}")
raise2. Isolate the Problem
# Use binary search to isolate
def debug_with_bisection(code_sections):
"""Find problematic section using bisection"""
if len(code_sections) == 1:
return code_sections[0]
mid = len(code_sections) // 2
try:
test_sections(code_sections[:mid])
# Error in second half
return debug_with_bisection(code_sections[mid:])
except:
# Error in first half
return debug_with_bisection(code_sections[:mid])3. AI-Assisted Debugging
# Use Claude to analyze errors
claude -p "Analyze this stack trace and suggest fixes: [paste trace]"
# Extended thinking for complex issues
claude --thinking "Debug this intermittent race condition"
# Pattern recognition
claude -p "What patterns do you see in these error logs?"Interactive Debugging Session
# Create debug hooks
class DebugHooks:
def __init__(self):
self.breakpoints = {}
def add_breakpoint(self, file, line):
"""Add interactive breakpoint"""
self.breakpoints[f"{file}:{line}"] = True
def check_breakpoint(self, file, line, locals_dict):
"""Check if we should break"""
if f"{file}:{line}" in self.breakpoints:
print(f"Breakpoint at {file}:{line}")
self.interactive_debug(locals_dict)
def interactive_debug(self, locals_dict):
"""Interactive debugging session"""
while True:
cmd = input("(debug) ")
if cmd == "continue":
break
elif cmd.startswith("print "):
var = cmd[6:]
if var in locals_dict:
print(f"{var} = {locals_dict[var]}")
elif cmd == "locals":
for k, v in locals_dict.items():
print(f"{k} = {v}")3. Error Recovery Patterns
Retry Strategies
Exponential Backoff with Jitter
import random
import time
from functools import wraps
def retry_with_backoff(max_retries=3, base_delay=1):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(max_retries):
try:
return func(*args, **kwargs)
except Exception as e:
if attempt == max_retries - 1:
raise
delay = base_delay * (2 ** attempt)
jitter = random.uniform(0, delay * 0.1)
wait_time = delay + jitter
print(f"Attempt {attempt + 1} failed: {e}")
print(f"Retrying in {wait_time:.2f}s...")
time.sleep(wait_time)
return wrapper
return decorator
@retry_with_backoff(max_retries=3)
def call_claude_api():
# API call that might fail
passCircuit Breaker Pattern
class CircuitBreaker:
def __init__(self, failure_threshold=5, recovery_timeout=60):
self.failure_threshold = failure_threshold
self.recovery_timeout = recovery_timeout
self.failure_count = 0
self.last_failure_time = None
self.state = "CLOSED" # CLOSED, OPEN, HALF_OPEN
def call(self, func, *args, **kwargs):
if self.state == "OPEN":
if time.time() - self.last_failure_time > self.recovery_timeout:
self.state = "HALF_OPEN"
else:
raise Exception("Circuit breaker is OPEN")
try:
result = func(*args, **kwargs)
if self.state == "HALF_OPEN":
self.state = "CLOSED"
self.failure_count = 0
return result
except Exception as e:
self.failure_count += 1
self.last_failure_time = time.time()
if self.failure_count >= self.failure_threshold:
self.state = "OPEN"
raise eFallback Strategies
class ModelFallback:
def __init__(self):
self.models = [
"claude-4-opus-20250514",
"claude-3-5-sonnet-20241022",
"claude-3-haiku-20240307"
]
async def generate_with_fallback(self, prompt):
"""Try models in order until one succeeds"""
errors = []
for model in self.models:
try:
return await self.generate(prompt, model)
except anthropic.RateLimitError as e:
errors.append(f"{model}: Rate limited")
except anthropic.APIError as e:
errors.append(f"{model}: {e}")
raise Exception(f"All models failed: {errors}")4. Logging and Error Tracking
Structured Logging
import structlog
import json
from datetime import datetime
# Configure structured logging
structlog.configure(
processors=[
structlog.stdlib.filter_by_level,
structlog.stdlib.add_logger_name,
structlog.stdlib.add_log_level,
structlog.stdlib.PositionalArgumentsFormatter(),
structlog.processors.TimeStamper(fmt="iso"),
structlog.processors.StackInfoRenderer(),
structlog.processors.format_exc_info,
structlog.processors.UnicodeDecoder(),
structlog.processors.JSONRenderer()
],
context_class=dict,
logger_factory=structlog.stdlib.LoggerFactory(),
cache_logger_on_first_use=True,
)
logger = structlog.get_logger()
# Log with context
logger.info("api_call_started",
model="claude-3-5-sonnet",
tokens=1500,
user_id="user123"
)
# Log errors with full context
try:
result = risky_operation()
except Exception as e:
logger.error("operation_failed",
error=str(e),
error_type=type(e).__name__,
stack_trace=traceback.format_exc(),
context={
"input": input_data,
"config": current_config
}
)Error Tracking Integration
# Sentry integration for production
import sentry_sdk
from sentry_sdk.integrations.logging import LoggingIntegration
sentry_sdk.init(
dsn="your-sentry-dsn",
integrations=[
LoggingIntegration(
level=logging.INFO,
event_level=logging.ERROR
)
],
traces_sample_rate=1.0,
profiles_sample_rate=1.0,
)
# Custom error enrichment
def before_send(event, hint):
# Add Claude Code specific context
event['extra']['claude_model'] = os.getenv('CLAUDE_MODEL')
event['extra']['token_usage'] = get_current_token_usage()
return event
sentry_sdk.init(before_send=before_send)OpenTelemetry Integration
from opentelemetry import trace
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
# Setup tracing
trace.set_tracer_provider(TracerProvider())
tracer = trace.get_tracer(__name__)
# Add OTLP exporter
otlp_exporter = OTLPSpanExporter(
endpoint="localhost:4317",
insecure=True
)
span_processor = BatchSpanProcessor(otlp_exporter)
trace.get_tracer_provider().add_span_processor(span_processor)
# Instrument Claude Code calls
@tracer.start_as_current_span("claude_api_call")
def call_claude(prompt):
span = trace.get_current_span()
span.set_attribute("claude.model", model)
span.set_attribute("claude.prompt_tokens", count_tokens(prompt))
try:
response = client.messages.create(...)
span.set_attribute("claude.response_tokens", response.usage.output_tokens)
return response
except Exception as e:
span.record_exception(e)
span.set_status(trace.Status(trace.StatusCode.ERROR))
raise5. Debugging Tools and Techniques
Claude Code Built-in Tools
# Diagnostic commands
claude /doctor # Fix configuration issues
claude /permissions # Check tool permissions
claude /mcp # Debug MCP servers
claude /status # System status check
# Session management
claude /clear # Clear context for fresh start
claude /compact # Compress conversation
claude /export # Export for analysis
# Error reporting
claude /bug # Report issues directlyCustom Debug Hooks
// .claude/hooks/debug-logger.js
{
"name": "debug-logger",
"version": "1.0.0",
"hooks": {
"before-prompt-submit": "log_prompt",
"after-response": "log_response",
"on-error": "log_error"
}
}// Implementation
export function log_prompt(data) {
const timestamp = new Date().toISOString();
const logEntry = {
timestamp,
type: "prompt",
content: data.prompt,
context_size: data.context_tokens
};
fs.appendFileSync('claude-debug.log',
JSON.stringify(logEntry) + '\n'
);
}
export function log_error(error) {
const errorLog = {
timestamp: new Date().toISOString(),
type: "error",
error: {
message: error.message,
stack: error.stack,
code: error.code
}
};
fs.appendFileSync('claude-errors.log',
JSON.stringify(errorLog) + '\n'
);
}Session Recording and Replay
class SessionRecorder:
def __init__(self, session_id):
self.session_id = session_id
self.events = []
def record_event(self, event_type, data):
event = {
"timestamp": time.time(),
"type": event_type,
"data": data
}
self.events.append(event)
def save_session(self):
filename = f"session_{self.session_id}.json"
with open(filename, 'w') as f:
json.dump(self.events, f, indent=2)
def replay_session(self, session_file):
"""Replay a recorded session for debugging"""
with open(session_file, 'r') as f:
events = json.load(f)
for event in events:
print(f"[{event['timestamp']}] {event['type']}")
if event['type'] == 'api_call':
# Replay the API call
self.replay_api_call(event['data'])
time.sleep(0.5) # Slow replay for analysis6. Common Pitfalls and Solutions
Context Window Management
class ContextManager:
def __init__(self, max_tokens=150000):
self.max_tokens = max_tokens
self.conversation = []
def add_message(self, role, content):
tokens = self.count_tokens(content)
self.conversation.append({
"role": role,
"content": content,
"tokens": tokens
})
# Auto-compress if needed
if self.total_tokens() > self.max_tokens * 0.8:
self.compress_conversation()
def compress_conversation(self):
"""Use Claude to summarize conversation"""
summary_prompt = "Summarize this conversation preserving key points"
# ImplementationPermission Issues
# Fix npm permissions
sudo chown -R $(whoami) ~/.npm
npm config set prefix ~/.npm
# Fix file permissions
chmod +x .claude/hooks/*.js
# Check Claude permissions
claude /permissionsCost Management
class CostMonitor:
def __init__(self, budget_limit=100):
self.budget_limit = budget_limit
self.current_spend = 0
def track_usage(self, model, input_tokens, output_tokens):
cost = self.calculate_cost(model, input_tokens, output_tokens)
self.current_spend += cost
if self.current_spend > self.budget_limit * 0.8:
self.send_alert(f"80% of budget used: ${self.current_spend}")
if self.current_spend > self.budget_limit:
raise BudgetExceededError(f"Budget exceeded: ${self.current_spend}")7. Production Error Handling
Error Handling Architecture
class ProductionErrorHandler:
def __init__(self):
self.error_queue = Queue()
self.handlers = {
'critical': self.handle_critical,
'warning': self.handle_warning,
'info': self.handle_info
}
async def process_errors(self):
"""Main error processing loop"""
while True:
error = await self.error_queue.get()
severity = self.classify_error(error)
handler = self.handlers.get(severity)
if handler:
await handler(error)
def classify_error(self, error):
"""Classify error severity"""
if isinstance(error, (AuthenticationError, SystemError)):
return 'critical'
elif isinstance(error, RateLimitError):
return 'warning'
return 'info'Monitoring Stack
# docker-compose.yml for monitoring
version: '3.8'
services:
prometheus:
image: prom/prometheus
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
ports:
- "9090:9090"
grafana:
image: grafana/grafana
ports:
- "3000:3000"
environment:
- GF_SECURITY_ADMIN_PASSWORD=admin
loki:
image: grafana/loki
ports:
- "3100:3100"
jaeger:
image: jaegertracing/all-in-one
ports:
- "16686:16686"
- "14268:14268"Best Practices Summary
- Fail Gracefully: Always provide fallback behavior
- Log Comprehensively: Capture context for debugging
- Monitor Proactively: Set up alerts before issues occur
- Test Error Paths: Unit test error handling code
- Document Issues: Update CLAUDE.md with known issues
- Use Built-in Tools: Leverage Claude’s debugging commands
- Implement Retries: Use exponential backoff wisely
- Track Costs: Monitor token usage continuously
Quick Reference
Debug Commands
claude /doctor # Fix configuration
claude /permissions # Check permissions
claude /clear # Clear context
claude /compact # Compress conversation
claude /bug # Report issuesEnvironment Variables
# Debugging
export ANTHROPIC_LOG=debug
export CLAUDE_DEBUG=true
export CLAUDE_TRACE_ERRORS=true
# Error handling
export CLAUDE_RETRY_MAX=5
export CLAUDE_RETRY_DELAY=1000See Also
- Claude Code CLI Reference - CLI commands and options
- Rate Limiting Guide - Handle rate limit errors
- Testing Guide - Test error handling
- Monitoring Guide - Production monitoring setup