10 KiB
10 KiB
Error Handling Guidelines
Version: 1.0 Last Updated: 2025-09-26 Purpose: Maintain consistent, debuggable error handling across the codebase
Quick Reference
✅ DO
# Specific exception handling with chaining
try:
result = api_call()
except GiteaNotFoundError as e:
raise IssueError(f"Issue #{number} not found") from e
except GiteaAuthError as e:
raise IssueError(f"Authentication failed") from e
# Logging for unexpected errors
except Exception as e:
logger.error(f"Unexpected error in {operation}", exc_info=True)
raise DomainError(f"Operation failed: {operation}") from e
❌ DON'T
# Overly broad exception handling
try:
result = api_call()
except Exception as e: # Too broad!
raise IssueError(f"Failed: {e}")
# Silent error suppression
try:
process_file()
except Exception:
continue # Never do this!
1. Exception Hierarchy
Use Domain-Specific Exceptions
Markitect Operations:
from markitect.exceptions import (
MarkitectError, # Base for all Markitect operations
DocumentError, # Document processing errors
ASTError, # AST parsing/processing errors
CacheError, # Cache operations errors
DatabaseError, # Database operation errors
SchemaError, # Schema validation/processing
ValidationError, # Document validation errors
GraphQLError, # GraphQL operations
ConfigurationError # Configuration/setup errors
)
TDDAI Operations:
from tddai.exceptions import (
TddaiError, # Base for TDDAI operations
WorkspaceError, # Workspace management
IssueError, # Issue fetching/management
TestGenerationError, # Test generation
ConfigurationError # Configuration issues
)
Gitea Operations:
from gitea.exceptions import (
GiteaError, # Base Gitea error
GiteaNotFoundError,# 404 responses
GiteaAuthError, # Authentication failures
GiteaApiError, # API errors with status codes
GiteaConfigError # Configuration issues
)
2. Exception Translation Patterns
Service Layer Pattern
Services should translate external exceptions to domain exceptions:
class IssueService:
def get_issue(self, issue_number: int) -> Issue:
"""Get issue by number.
Raises:
IssueError: When issue cannot be retrieved
"""
try:
return self.gitea_client.issues.get(issue_number)
except GiteaNotFoundError as e:
raise IssueError(f"Issue #{issue_number} not found") from e
except GiteaAuthError as e:
raise IssueError(f"Authentication failed") from e
except GiteaApiError as e:
raise IssueError(f"API error: {e}") from e
# Don't catch GiteaError - let specific exceptions handle it
File Operations Pattern
def read_config_file(file_path: Path) -> dict:
"""Read configuration file.
Raises:
ConfigurationError: When file cannot be read or parsed
"""
try:
content = file_path.read_text()
return json.loads(content)
except FileNotFoundError as e:
raise ConfigurationError(f"Config file not found: {file_path}") from e
except PermissionError as e:
raise ConfigurationError(f"Permission denied reading: {file_path}") from e
except json.JSONDecodeError as e:
raise ConfigurationError(f"Invalid JSON in {file_path}: {e}") from e
except Exception as e:
logger.error(f"Unexpected error reading {file_path}", exc_info=True)
raise ConfigurationError(f"Failed to read config: {file_path}") from e
3. Exception Chaining Rules
Always Chain Exceptions
Use raise ... from e to preserve the original exception:
# ✅ CORRECT - preserves debugging information
try:
dangerous_operation()
except SpecificError as e:
raise DomainError("User-friendly message") from e
# ❌ WRONG - loses original exception context
try:
dangerous_operation()
except SpecificError as e:
raise DomainError(f"Failed: {e}")
Chain Standard Exceptions
try:
data = json.loads(content)
except json.JSONDecodeError as e:
raise ValidationError(f"Invalid JSON format") from e
except (ValueError, TypeError) as e:
raise ValidationError(f"Data validation failed") from e
4. Logging Integration
Log Before Re-raising
import logging
logger = logging.getLogger(__name__)
try:
complex_operation()
except ExpectedError as e:
# Don't log expected errors - let caller decide
raise DomainError("Operation failed") from e
except Exception as e:
# Always log unexpected errors
logger.error(
"Unexpected error in complex_operation",
extra={'context': {'param1': value1}},
exc_info=True
)
raise DomainError("Unexpected failure") from e
Logging Levels
- ERROR: Unexpected exceptions that indicate bugs
- WARNING: Expected exceptions that are concerning (file not found, permission denied)
- INFO: Normal error recovery (retries, fallbacks)
- DEBUG: Detailed error context for development
5. Error Messages
User-Facing Messages
# ✅ GOOD - actionable and specific
raise IssueError(f"Issue #{number} not found. Check the issue number and try again.")
# ✅ GOOD - includes context
raise ConfigurationError(f"Missing required setting 'api_token' in {config_file}")
# ❌ BAD - too technical
raise IssueError(f"HTTP 404 response from /api/v1/repos/owner/repo/issues/{number}")
# ❌ BAD - too vague
raise IssueError("Something went wrong")
Include Context
# Use MarkitectError's context parameter
raise DocumentError(
"Failed to parse document",
cause=original_error,
context={
'file_path': str(file_path),
'line_number': line_num,
'operation': 'parse_markdown'
}
)
6. CLI Error Handling
Consistent CLI Pattern
def cli_command():
"""CLI command that handles domain exceptions."""
try:
result = service.perform_operation()
show_success(result)
except DomainError as e:
OutputFormatter.exit_with_error(str(e))
# Don't catch Exception - let unexpected errors bubble up
Exit Codes
- 0: Success
- 1: Expected failure (user error, missing resource)
- 2: Configuration error
- >2: Unexpected error (let Python handle it)
7. Anti-Patterns to Avoid
1. Overly Broad Exception Handling
# ❌ NEVER DO THIS
try:
operation()
except Exception as e:
raise DomainError(f"Failed: {e}")
2. Silent Error Suppression
# ❌ NEVER DO THIS
try:
process_file(file)
except Exception:
continue # Silent failure!
# ✅ DO THIS INSTEAD
try:
process_file(file)
except (OSError, IOError) as e:
logger.warning(f"Could not process {file}: {e}")
continue
except Exception as e:
logger.error(f"Unexpected error processing {file}: {e}", exc_info=True)
continue
3. Exception Conversion Without Context
# ❌ WRONG - loses information
try:
api_call()
except requests.RequestException as e:
raise IssueError("API failed")
# ✅ CORRECT - preserves context
try:
api_call()
except requests.ConnectionError as e:
raise IssueError("Cannot connect to Gitea server") from e
except requests.Timeout as e:
raise IssueError("Gitea server request timed out") from e
except requests.HTTPError as e:
raise IssueError(f"HTTP error: {e.response.status_code}") from e
8. Testing Error Handling
Test Exception Translation
def test_issue_not_found():
"""Test that GiteaNotFoundError is translated to IssueError."""
with mock.patch.object(gitea_client, 'get') as mock_get:
mock_get.side_effect = GiteaNotFoundError("Not found")
with pytest.raises(IssueError) as exc_info:
service.get_issue(123)
assert "Issue #123 not found" in str(exc_info.value)
assert exc_info.value.__cause__.__class__ == GiteaNotFoundError
Test Error Messages
def test_meaningful_error_messages():
"""Test that error messages are user-friendly."""
with pytest.raises(ConfigurationError) as exc_info:
service.load_config("nonexistent.json")
error_msg = str(exc_info.value)
assert "nonexistent.json" in error_msg
assert "not found" in error_msg.lower()
9. Refactoring Checklist
When refactoring error handling, use this checklist:
🔍 Identify Issues
- Search for
except Exception:patterns - Look for
continuewithout logging in exception blocks - Find missing exception chaining (
raise ... from e) - Check for generic error messages
🔧 Fix Patterns
- Replace broad exceptions with specific ones
- Add proper exception chaining
- Implement logging for unexpected errors
- Improve error message clarity
- Add exception documentation to functions
✅ Verify
- Test that CLI still works
- Verify error messages are user-friendly
- Check that debugging information is preserved
- Ensure no silent failures remain
📚 Document
- Update function docstrings with
Raises:sections - Add new exceptions to relevant
__init__.pyfiles - Update this guide if new patterns emerge
10. Common Search Patterns
Use these patterns to find error handling issues:
# Find overly broad exception handling
rg "except Exception" --type py
# Find silent error suppression
rg "except.*:\s*continue" --type py
rg "except.*:\s*pass" --type py
# Find missing exception chaining
rg "raise.*Error.*:" --type py | grep -v "from"
# Find exception handling without logging
rg "except.*Exception.*:" -A 3 --type py | grep -v "log"
11. Quick Migration Template
Use this template for migrating old exception handling:
# OLD PATTERN
try:
operation()
except Exception as e:
raise DomainError(f"Operation failed: {e}")
# NEW PATTERN
try:
operation()
except SpecificError1 as e:
raise DomainError(f"Specific failure case 1") from e
except SpecificError2 as e:
raise DomainError(f"Specific failure case 2") from e
except Exception as e:
logger.error("Unexpected error in operation", exc_info=True)
raise DomainError(f"Unexpected operation failure") from e
Remember: Good error handling makes debugging easier, provides better user experience, and prevents silent failures that hide bugs. When in doubt, be specific, preserve context, and log unexpected errors.