# 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 ```