# Production Error Handling Patterns

## Exception Hierarchy
```python
# exceptions.py
class AppError(Exception):
    """Base exception for application."""
    def __init__(self, message: str, code: str, **details):
        super().__init__(message)
        self.message = message
        self.code = code
        self.details = details

class ValidationError(AppError):
    """Invalid input data."""
    def __init__(self, message: str, field: str = None, **details):
        super().__init__(message, "VALIDATION_ERROR", field=field, **details)

class NetworkError(AppError):
    """Network-related errors."""
    def __init__(self, message: str, status_code: int = None, **details):
        super().__init__(message, "NETWORK_ERROR", status_code=status_code, **details)

class AuthenticationError(AppError):
    """Authentication failed."""
    def __init__(self, message: str = "Authentication required"):
        super().__init__(message, "AUTH_ERROR")
```

## Error Handling Patterns

### 1. Wrap Low-Level Errors
```python
def fetch_user(user_id: str) -> dict:
    """Fetch user with proper error handling."""
    try:
        response = requests.get(f"/api/users/{user_id}")
        response.raise_for_status()
        return response.json()
    except requests.ConnectionError as e:
        raise NetworkError(
            f"Failed to connect to user service",
            service="user-api"
        ) from e
    except requests.HTTPError as e:
        if e.response.status_code == 404:
            raise ResourceNotFoundError(f"User {user_id} not found") from e
        raise NetworkError(
            f"User service error",
            status_code=e.response.status_code
        ) from e
```

### 2. Context Accumulation
```python
def process_order(order_id: str) -> Order:
    """Process order with context accumulation."""
    try:
        order = fetch_order(order_id)
        validate_order(order)
        return fulfill_order(order)
    except AppError as e:
        # Add context as error bubbles up
        e.details['order_id'] = order_id
        e.details['step'] = 'process_order'
        logger.error(f"Order processing failed: {e}", extra=e.details)
        raise
```

### 3. Error Recovery
```python
from functools import wraps
from typing import TypeVar, Callable

T = TypeVar('T')

def with_fallback(fallback_value: T) -> Callable:
    """Decorator to provide fallback on error."""
    def decorator(func: Callable[..., T]) -> Callable[..., T]:
        @wraps(func)
        def wrapper(*args, **kwargs) -> T:
            try:
                return func(*args, **kwargs)
            except Exception as e:
                logger.warning(f"{func.__name__} failed, using fallback: {e}")
                return fallback_value
        return wrapper
    return decorator

@with_fallback(fallback_value=[])
def get_recommendations(user_id: str) -> list[str]:
    """Get recommendations with fallback to empty list."""
    return recommendation_service.get(user_id)
```

### 4. Retry with Error Types
```python
def retry_on_errors(*error_types, max_attempts=3, delay=1.0):
    """Retry only on specific error types."""
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            last_error = None
            for attempt in range(max_attempts):
                try:
                    return func(*args, **kwargs)
                except error_types as e:
                    last_error = e
                    if attempt < max_attempts - 1:
                        time.sleep(delay * (2 ** attempt))
                    continue
                except Exception:
                    # Don't retry other exceptions
                    raise
            raise last_error
        return wrapper
    return decorator

@retry_on_errors(NetworkError, TimeoutError, max_attempts=3)
def call_external_api():
    """Will retry on network errors only."""
    return api.call()
```

### 5. Error Aggregation
```python
class ErrorCollector:
    """Collect multiple errors for batch operations."""

    def __init__(self):
        self.errors = []

    def add(self, error: Exception, context: dict = None):
        self.errors.append({
            'error': error,
            'context': context or {},
            'timestamp': datetime.now()
        })

    def raise_if_errors(self):
        if self.errors:
            raise MultiError(self.errors)

# Usage
def process_batch(items: list) -> list:
    collector = ErrorCollector()
    results = []

    for item in items:
        try:
            results.append(process_item(item))
        except ValidationError as e:
            collector.add(e, {'item_id': item.id})

    # Process all items before raising
    collector.raise_if_errors()
    return results
```

## API Error Responses

### 1. Consistent Error Format
```python
from flask import jsonify

def error_response(error: AppError) -> Response:
    """Create consistent error response."""
    return jsonify({
        'error': {
            'code': error.code,
            'message': error.message,
            'details': error.details
        }
    }), get_status_code(error)

@app.errorhandler(AppError)
def handle_app_error(error):
    return error_response(error)
```

### 2. Error Code Mapping
```python
ERROR_STATUS_CODES = {
    'VALIDATION_ERROR': 400,
    'AUTH_ERROR': 401,
    'FORBIDDEN': 403,
    'NOT_FOUND': 404,
    'NETWORK_ERROR': 503,
}

def get_status_code(error: AppError) -> int:
    return ERROR_STATUS_CODES.get(error.code, 500)
```

## Testing Error Handling
```python
import pytest

def test_network_error_handling():
    with pytest.raises(NetworkError) as exc_info:
        fetch_data_from_unreliable_service()

    error = exc_info.value
    assert error.code == "NETWORK_ERROR"
    assert error.details.get('service') == 'external-api'

def test_error_recovery():
    # Mock the service to fail twice then succeed
    with patch('api.call') as mock_call:
        mock_call.side_effect = [
            NetworkError("Timeout"),
            NetworkError("Timeout"),
            {"data": "success"}
        ]

        result = call_external_api()  # Has retry decorator
        assert result == {"data": "success"}
        assert mock_call.call_count == 3
```

## Best Practices
1. **Specific over generic**: Create specific exception types
2. **Preserve context**: Use exception chaining (`raise ... from e`)
3. **Log at boundaries**: Log errors when catching, not when raising
4. **Fail fast**: Validate early and raise immediately
5. **Graceful degradation**: Provide fallbacks for non-critical features
