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

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 variable

Common 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 /cost command
  • Use token bucket algorithm

Bad Request Errors (400)

except anthropic.BadRequestError as e:
    print(f"Invalid request: {e}")
    # Check prompt format, model name, parameters

Claude Code Specific Errors

Tool Execution Errors

# Common tool errors
- Permission denied for file operations
- Command timeout exceeded
- Invalid file paths
- Network access restrictions

Debug Commands:

# Check permissions
claude /permissions
 
# Fix configuration issues
claude /doctor
 
# Review tool settings
claude /config

Context 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 False

2. 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}")
        raise

2. 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
    pass

Circuit 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 e

Fallback 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))
        raise

5. 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 directly

Custom 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 analysis

6. 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"
        # Implementation

Permission 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 /permissions

Cost 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

  1. Fail Gracefully: Always provide fallback behavior
  2. Log Comprehensively: Capture context for debugging
  3. Monitor Proactively: Set up alerts before issues occur
  4. Test Error Paths: Unit test error handling code
  5. Document Issues: Update CLAUDE.md with known issues
  6. Use Built-in Tools: Leverage Claude’s debugging commands
  7. Implement Retries: Use exponential backoff wisely
  8. 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 issues

Environment 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=1000

See Also