Custom Error Handling
Custom error handling provides a standardized mechanism for applications to define, raise, and manage application-specific errors. This approach enhances clarity, simplifies debugging, and improves the user experience by providing consistent error information.
The CustomError Class
The CustomError class serves as the foundation for defining application-specific errors. It extends RuntimeUserError, ensuring that custom errors align with the system's user-facing error reporting conventions.
Instantiation
Instantiate CustomError by providing an error code and a descriptive message. The code typically represents a unique identifier for the error type, while the message offers human-readable context.
try:
# Simulate an operation that might fail based on a condition
condition_failed = True
if condition_failed:
raise CustomError(code="INVALID_CONFIGURATION", message="The application configuration is incomplete.")
except CustomError as e:
print(f"Caught custom error: Code={e.code}, Message='{e.message}'")
The CustomError constructor automatically categorizes the error as a "user" error, which is useful for distinguishing between system-level failures and issues directly attributable to user input or action.
Wrapping Existing Exceptions
The CustomError class offers a convenient way to wrap standard Python exceptions or other third-party exceptions into a CustomError instance using the from_exception class method. This method extracts the original exception's class name as the error code and its string representation as the error message. This capability is particularly useful when an underlying library raises a generic exception, and the application needs to present it consistently as a custom error.
import json
def parse_configuration(config_string: str):
"""
Parses a configuration string and validates its content.
Raises CustomError for parsing or validation failures.
"""
try:
config = json.loads(config_string)
# Example validation: check for a required key
if not isinstance(config, dict) or "api_key" not in config:
raise ValueError("Missing required 'api_key' in configuration.")
print("Configuration parsed and validated successfully.")
return config
except json.JSONDecodeError as e:
# Wrap JSON parsing errors into a CustomError
raise CustomError.from_exception(e)
except ValueError as e:
# Wrap validation errors into a CustomError
raise CustomError.from_exception(e)
# Common use case 1: Invalid JSON format
try:
parse_configuration("this is not valid json")
except CustomError as ce:
print(f"Error parsing configuration: Code={ce.code}, Message='{ce.message}'")
# Expected output: Error parsing configuration: Code=JSONDecodeError, Message='Expecting value: line 1 column 1 (char 0)'
print("-" * 30)
# Common use case 2: Valid JSON, but invalid content (missing required key)
try:
parse_configuration('{"database_url": "sqlite:///app.db"}')
except CustomError as ce:
print(f"Error parsing configuration: Code={ce.code}, Message='{ce.message}'")
# Expected output: Error parsing configuration: Code=ValueError, Message='Missing required 'api_key' in configuration.'
Integration and Best Practices
Raising Custom Errors
Raise CustomError instances at points where an application-specific condition prevents successful execution. This ensures that error handling logic can specifically target these known issues.
def process_order(order_id: str, quantity: int):
if not order_id or not order_id.isalnum():
raise CustomError(code="INVALID_ORDER_ID", message="Order ID must be alphanumeric and non-empty.")
if quantity <= 0:
raise CustomError(code="INVALID_QUANTITY", message="Order quantity must be a positive number.")
print(f"Processing order '{order_id}' with quantity {quantity}.")
try:
process_order("ORD123", 0)
except CustomError as e:
print(f"Order processing failed: {e.message} (Code: {e.code})")
try:
process_order("ORD456", 5)
except CustomError as e:
print(f"Order processing failed: {e.message} (Code: {e.code})")
Catching Custom Errors
Catch CustomError instances to implement specific recovery, logging, or user feedback mechanisms. Since CustomError inherits from RuntimeUserError, it can be caught specifically or as part of a broader RuntimeUserError catch block.
def execute_application_logic():
# Simulate a scenario where a custom error might be raised
if some_internal_check_fails: # Assume this is True for demonstration
raise CustomError(code="SERVICE_UNAVAILABLE", message="Dependent service is currently unreachable.")
# ... more logic ...
some_internal_check_fails = True # For demonstration purposes
try:
execute_application_logic()
except CustomError as e:
# Handle specific CustomError cases, e.g., inform the user, retry
print(f"Application-specific error caught: {e.code} - {e.message}")
# Log the error with specific details
except RuntimeUserError as e:
# Handle other user-facing runtime errors generically
print(f"Generic user error caught: {e.code} - {e.message}")
# Log and provide a general user error message
except Exception as e:
# Catch all other unexpected system-level errors
print(f"Unexpected system error: {e.__class__.__name__} - {e}")
# Log the full traceback and notify operations
Error Codes and Messages
- Error Codes: Use consistent, descriptive, and unique error codes (e.g.,
INVALID_INPUT,RESOURCE_NOT_FOUND,PERMISSION_DENIED). These codes facilitate programmatic error identification, internationalization, and automated error handling. - Error Messages: Craft clear, concise, and actionable error messages. For user-facing errors, messages should help the user understand what went wrong and, if possible, how to resolve it. Avoid overly technical jargon in user-facing messages.
Considerations
- Error Type:
CustomErroris designed for user-facing runtime issues. Avoid using it for system-level or unrecoverable errors that should typically propagate as standard exceptions or more specific system errors. - Performance: Raising and catching exceptions has a minor performance overhead. Use custom errors for truly exceptional conditions, not for routine control flow or expected branching logic.
- Integration with Frameworks: When integrating with web frameworks or other application frameworks, map
CustomErrorinstances to appropriate HTTP status codes (e.g., 400 Bad Request forINVALID_INPUT, 404 Not Found forRESOURCE_NOT_FOUND) or framework-specific error responses. This ensures consistent error presentation across different application layers.
Limitations
The CustomError class hardcodes the error type to "user". If different categories of custom errors (e.g., "system", "external_service") are required, consider extending RuntimeUserError directly or introducing an additional parameter to CustomError for the error type. For typical user-facing application errors, the "user" type is appropriate and sufficient.