# Error Handling Guide

This guide outlines Honcho's error handling strategy, providing consistent patterns for handling exceptions throughout the codebase.

## Core Principles

1. **User-friendly error messages**: Errors should be informative for API consumers but avoid exposing internal details.
2. **Consistent response format**: All API errors return JSON with a `detail` field.
3. **Appropriate status codes**: Use standard HTTP status codes consistently.
4. **Structured logging**: Log errors with context for troubleshooting.
5. **Centralized error management**: Define custom exceptions and handle them globally.

## Custom Exception Types

Honcho defines custom exception types in `src/exceptions.py`:

```python
class HonchoException(Exception):
    status_code = 500  # Default status code
    detail = "An unexpected error occurred"  # Default message

class ResourceNotFoundException(HonchoException):
    status_code = 404
    detail = "Resource not found"

class ValidationException(HonchoException):
    status_code = 422
    detail = "Validation error"

class ConflictException(HonchoException):
    status_code = 409
    detail = "Resource conflict"

class AuthenticationException(HonchoException):
    status_code = 401
    detail = "Authentication failed"

class AuthorizationException(HonchoException):
    status_code = 403
    detail = "Not authorized to access this resource"
```

## Error Handling by Layer

### CRUD Layer

Use specific custom exceptions rather than generic `ValueError` or returning `None`:

```python
# Good
async def get_user(db, app_id, user_id):
    user = await db.execute(stmt).scalar_one_or_none()
    if user is None:
        raise ResourceNotFoundException(f"User with ID {user_id} not found")
    return user

# Avoid
async def get_user(db, app_id, user_id):
    user = await db.execute(stmt).scalar_one_or_none()
    return user  # Returning None isn't explicit
```

For database integrity errors, catch and convert them to custom exceptions:

```python
try:
    db.add(user)
    await db.commit()
    return user
except IntegrityError as e:
    await db.rollback()
    logger.error(f"IntegrityError creating user: {str(e)}")
    raise ConflictException("User already exists") from e
```

### Router Layer

FastAPI routes should be simple, letting global exception handlers do most of the work:

```python
@router.get("/{user_id}")
async def get_user(app_id: str, user_id: str, db: AsyncSession = db):
    # ResourceNotFoundException will be caught by global handler
    return await crud.get_user(db, app_id, user_id)
```

For specific handling, use try/except only when necessary:

```python
@router.post("")
async def create_user(app_id: str, user: schemas.UserCreate, db: AsyncSession = db):
    try:
        return await crud.create_user(db, app_id, user)
    except ConflictException:
        # Example of capturing a specific exception for custom handling
        logger.warning(f"Conflict creating user with name: {user.name}")
        raise  # Re-raise to be handled by global handler
```

### Background Tasks and Queue Processing

Use structured logging and context in background tasks:

```python
try:
    await process_item(db, payload)
    logger.info(f"Successfully processed message {message_id}")
except Exception as e:
    logger.error(
        f"Error processing message {message_id}: {str(e)}",
        exc_info=True
    )
    if os.getenv("SENTRY_ENABLED", "False").lower() == "true":
        sentry_sdk.capture_exception(e)
```

## Logging Best Practices

1. **Use the standard logging module**:
   ```python
   logger = logging.getLogger(__name__)
   ```

2. **Include context in logs**:
   ```python
   logger.error(
       f"Failed to process message for app {app_id}, user {user_id}",
       exc_info=True
   )
   ```

3. **Log levels**:
   - `DEBUG`: Detailed information, useful for debugging
   - `INFO`: Confirmation of normal operation
   - `WARNING`: Unexpected situation that doesn't affect operation
   - `ERROR`: Problem preventing normal operation
   - `CRITICAL`: Application-wide failure

4. **Avoid print statements** - use proper logging instead.

## Error Reporting with Sentry

For production monitoring, we use Sentry:

```python
if os.getenv("SENTRY_ENABLED", "False").lower() == "true":
    sentry_sdk.capture_exception(e)
```

## Examples

### CRUD Function

```python
async def update_document(db, collection_id, document_id, document):
    """
    Update a document.

    Args:
        db: Database session
        collection_id: ID of the collection
        document_id: ID of the document
        document: Document update schema

    Returns:
        The updated document

    Raises:
        ResourceNotFoundException: If the document or collection does not exist
        ValidationException: If the document data is invalid
    """
    try:
        # Get document (raises ResourceNotFoundException if not found)
        honcho_document = await get_document(db, collection_id, document_id)

        # Update document data
        if document.content is not None:
            honcho_document.content = document.content
        if document.metadata is not None:
            honcho_document.h_metadata = document.metadata

        await db.commit()
        logger.info(f"Document {document_id} updated successfully")
        return honcho_document
    except IntegrityError as e:
        await db.rollback()
        logger.error(f"IntegrityError updating document {document_id}: {str(e)}")
        raise ValidationException("Document update failed - constraint violation") from e
```

### Router Endpoint

```python
@router.get("/{document_id}")
async def get_document(
    app_id: str,
    user_id: str,
    collection_id: str,
    document_id: str,
    db: AsyncSession = db
):
    """Get a document by ID"""
    # ResourceNotFoundException will be caught by global handler
    return await crud.get_document(db, collection_id, document_id)
```

### Background Processing

```python
async def process_item(db, payload):
    """Process a queue item"""
    try:
        # Validate required fields
        required_fields = ["content", "app_id", "user_id", "session_id", "message_id"]
        for field in required_fields:
            if field not in payload:
                logger.error(f"Missing required field in payload: {field}")
                raise ValidationException(f"Missing field: {field}")

        # Process the item
        await do_processing(db, payload)
        logger.info(f"Processed message {payload['message_id']}")
    except Exception as e:
        logger.error(f"Error processing message: {str(e)}", exc_info=True)
        if os.getenv("SENTRY_ENABLED", "False").lower() == "true":
            sentry_sdk.capture_exception(e)
        raise
```
