This document defines the standard exception handling patterns for the VLog codebase.
- Always re-raise HTTPException - Never mask HTTPExceptions as they contain proper status codes and error messages
- Use specific exception types - Catch specific exceptions when possible rather than broad
Exception - Log with context - Include operation name and relevant context in error logs
- Sanitize error messages - Use the
sanitize_error_message()utility to prevent leaking internal details - Maintain exception chains - Use
raise ... from eto preserve the original exception
Use when: You need to catch generic exceptions but want to preserve HTTPExceptions
try:
result = await some_operation()
except HTTPException:
raise # Always re-raise HTTP errors
except Exception as e:
logger.exception(f"Unexpected error in operation_name: {e}")
raise HTTPException(status_code=500, detail="Internal server error")Use when: You can predict specific exception types
try:
result = await database_operation()
except HTTPException:
raise
except (ValueError, KeyError, TypeError) as e:
logger.error(f"Validation error in operation_name: {e}")
raise HTTPException(status_code=400, detail=f"Invalid input: {str(e)}")
except DatabaseLockedError as e:
raise HTTPException(status_code=503, detail="Database temporarily unavailable")
except Exception as e:
logger.exception(f"Unexpected error in operation_name: {e}")
raise HTTPException(status_code=500, detail="Internal server error")Use when: You need to clean up resources on failure
resource_path = None
try:
resource_path = create_resource()
result = await process_resource(resource_path)
except HTTPException:
# Clean up on HTTP errors too
if resource_path:
cleanup_resource(resource_path)
raise
except Exception as e:
# Clean up on any error
if resource_path:
cleanup_resource(resource_path)
logger.exception(f"Error processing resource: {e}")
raise HTTPException(status_code=500, detail="Processing failed")Use when: You want standardized handling for an entire function
from api.exception_utils import handle_api_exceptions
@handle_api_exceptions("video_upload", "Failed to upload video", 500)
async def upload_video(...):
# Your code here - HTTPExceptions will be re-raised,
# other exceptions will be converted to 500 errors
result = await process_upload()
return resultUse when: Errors should be logged but not propagate
async def background_cleanup():
"""Non-critical background operation."""
try:
await cleanup_old_files()
except Exception as e:
# Log but don't propagate - this is a background task
logger.exception(f"Error in background cleanup: {e}")
# Don't raise - let the task continueUse when: You want detailed logs but sanitized user-facing errors
from api.errors import sanitize_error_message
try:
result = await transcode_video()
except HTTPException:
raise
except Exception as e:
# Log the full error internally
logger.exception(f"Transcoding failed for video {video_id}: {e}")
# Send sanitized error to user
sanitized = sanitize_error_message(str(e), context=f"video_id={video_id}")
raise HTTPException(status_code=500, detail=sanitized)# BAD - HTTPException gets masked
try:
await operation()
except Exception as e: # This catches HTTPException too!
raise HTTPException(status_code=500, detail="Error")# BAD - Error swallowed with no logging
try:
await operation()
except Exception:
pass # Silent failure# BAD - Exposes internal file paths
try:
await process_file(path)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e)) # May contain /home/user/...# BAD - Should catch ValueError specifically
try:
value = int(user_input)
except Exception as e: # Too broad
raise HTTPException(status_code=400, detail="Invalid input")When updating existing code:
- Identify broad Exception handlers - Look for
except Exceptionblocks - Add HTTPException re-raise - Add
except HTTPException: raisebefore the Exception block - Consider specific exceptions - Can you catch more specific exception types?
- Add logging - Use
logger.exception()for unexpected errors - Sanitize messages - Use
sanitize_error_message()for user-facing errors - Test the changes - Verify HTTPExceptions still propagate correctly
Before:
try:
result = await operation()
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))After:
try:
result = await operation()
except HTTPException:
raise # Don't mask HTTP errors
except ValueError as e:
logger.error(f"Validation error in operation: {e}")
raise HTTPException(status_code=400, detail="Invalid input")
except Exception as e:
logger.exception(f"Unexpected error in operation: {e}")
raise HTTPException(status_code=500, detail="Internal server error")All exception handling should be tested:
- Test HTTPException propagation - Verify HTTPExceptions are not masked
- Test error conversion - Verify generic exceptions become HTTPExceptions
- Test error messages - Verify messages are sanitized and user-friendly
- Test cleanup - Verify resources are cleaned up on error
- Test logging - Verify errors are logged with appropriate context
See tests/test_exception_handling.py for examples.
api/exception_utils.py- Exception handling decorators and utilitiesapi/errors.py- Error message sanitizationapi/db_retry.py- Database-specific retry logic
If you're unsure about the right pattern for a specific case, consider:
- Is this a user-facing API endpoint? → Use Pattern 1 or 2
- Is this a background task? → Use Pattern 5
- Do I need resource cleanup? → Use Pattern 3
- Is the entire function critical? → Use Pattern 4 decorator
- Do I know the specific exceptions? → Use Pattern 2
When in doubt, Pattern 1 (basic with HTTPException re-raise) is a safe default.