Skip to content

Error Handling Guide

castlecraft-engineer defines a set of custom exceptions to signal specific error conditions that can occur when using its various components. Understanding these exceptions and how to handle them is crucial for building robust applications.

Most custom exceptions are defined in their respective modules (e.g., abstractions/aggregate.py, common/crypto.py) and are conveniently re-exported from castlecraft_engineer.exc for easier importing.

# Example of importing common exceptions
from castlecraft_engineer.exc import (
    CommandHandlerNotFoundError,
    EventHandlerRegistrationError,
    QueryHandlerNotFoundError,
    AggregateNotFoundError,
    OptimisticConcurrencyError,
    RepositoryError,
    AuthorizationError,
    InvalidEncryptionFormat,
    InvalidEncryptionKey,
)

1. Repository Errors

These exceptions are typically raised by AggregateRepository or AsyncAggregateRepository (from castlecraft_engineer.abstractions.aggregate) when interacting with the persistence layer for aggregates.

  • RepositoryError(Exception): A generic base class for errors originating from repository operations. You might catch this if you want a general handler for any repository-related issue, though catching more specific exceptions is often better.

  • AggregateNotFoundError(RepositoryError):

    • When Raised: When an operation attempts to retrieve or modify an aggregate that does not exist in the database (e.g., get_by_id fails to find an entity, or an update/delete operation targets a non-existent ID).
    • Handling: This often translates to an HTTP 404 Not Found response in web applications. You might log the attempt and inform the user that the requested resource doesn't exist.
      # try:
      #     product = await product_repo.get_by_id(session, product_id)
      #     if not product: # Or rely on get_by_id raising it if it's designed to
      #         raise AggregateNotFoundError(f"Product with ID {product_id} not found.")
      # except AggregateNotFoundError as e:
      #     # Log error, return 404 response
      #     print(f"Error: {e}")
      
  • OptimisticConcurrencyError(RepositoryError):

    • When Raised: During a save operation on an aggregate if the version of the aggregate in the database is different from the version of the aggregate instance being saved. This indicates that the aggregate was modified by another process since it was last loaded.
    • Handling: This is a critical error to handle to prevent lost updates. Strategies include:
      • Retrying the operation (fetching the latest version of the aggregate, reapplying changes, and attempting to save again).
      • Informing the user that the data has changed and asking them to review and resubmit their changes.
      • Returning an HTTP 409 Conflict response.
        # try:
        #     # ... load aggregate, make changes ...
        #     await product_repo.save(session, updated_product)
        # except OptimisticConcurrencyError as e:
        #     # Log conflict, inform user, potentially retry or return 409
        #     print(f"Conflict saving product: {e}. Please refresh and try again.")
        

2. CQRS Bus Errors

These exceptions are raised by the CommandBus, QueryBus, or EventBus when there are issues with handler registration or dispatch.

  • CommandHandlerNotFoundError(Exception) (from castlecraft_engineer.abstractions.command_bus):

    • When Raised: When command_bus.execute(command) is called, but no command handler has been registered for the type of the given command.
    • Handling: This usually indicates a configuration or programming error (a command was defined but its handler wasn't registered with the DI container and bus). Log this as a server-side error (e.g., HTTP 500). During development, this helps catch missing registrations.
  • QueryHandlerNotFoundError(Exception) (from castlecraft_engineer.abstractions.query_bus):

    • When Raised: When query_bus.execute(query) is called, but no query handler has been registered for the type of the given query.
    • Handling: Similar to CommandHandlerNotFoundError, this points to a setup issue. Log as a server-side error.
  • EventHandlerRegistrationError(Exception) (from castlecraft_engineer.abstractions.event_bus):

    • When Raised: If there's an issue during the registration of an event handler with the EventBus (e.g., trying to register multiple handlers for an event type if the bus is configured for single handlers, or other internal registration issues).
    • Handling: This error typically occurs at application startup during DI container configuration. It should be treated as a critical setup failure.
# try:
#     await command_bus.execute(some_command)
# except CommandHandlerNotFoundError as e:
#     # Log this as a server error (500) - indicates a misconfiguration
#     print(f"Critical error: {e}")
# except QueryHandlerNotFoundError as e:
#     # Log this as a server error (500)
#     print(f"Critical error: {e}")

3. Authorization Errors

  • AuthorizationError(Exception) (from castlecraft_engineer.authorization.base_service):
    • When Raised: By an AuthorizationService's check_permission method when the current actor (user/process) does not have the required permissions to perform an action or access a resource. This is also raised by the authorize method of Command/Query Handlers if the permission check fails.
    • Handling: This should typically result in an HTTP 403 Forbidden response, indicating that the request was understood, but the server refuses to authorize it. Log the failed authorization attempt for security auditing.
      # In a Command/Query Handler's authorize method or when calling auth_service directly:
      # try:
      #     await auth_service.check_permission(required_permissions=[...])
      # except AuthorizationError as e:
      #     # Log unauthorized access attempt, return 403 response
      #     print(f"Forbidden: {e}")
      #     raise # Re-raise for the bus to handle or for an API framework to catch
      

4. Cryptography Errors

These exceptions are raised by the encryption and decryption utilities in castlecraft_engineer.common.crypto.

  • InvalidEncryptionKey(ValueError):

    • When Raised: If the SECRET_ENCRYPTION_KEY environment variable is missing, not a valid hexadecimal string, or not the correct length (32 bytes / 64 hex characters for AES-256).
    • Handling: This is a critical configuration error. The application should likely fail to start or operate correctly if encryption/decryption is essential. Log the error and ensure the key is configured properly in the environment.
  • InvalidEncryptionFormat(ValueError):

    • When Raised: During decryption (decrypt_data) if:
      • The provided encrypted data is malformed or not in the expected hex format.
      • The decryption fails due to an incorrect key being used.
      • The data integrity check (part of AES-GCM) fails, indicating the ciphertext may have been tampered with.
    • Handling: This can indicate corrupted data, an attempt to decrypt with the wrong key, or potential tampering. Log the error details. Depending on the context, you might treat the data as invalid or unavailable. Avoid exposing detailed crypto errors to end-users.
from castlecraft_engineer.common.crypto import encrypt_data, decrypt_data
from castlecraft_engineer.exc import InvalidEncryptionKey, InvalidEncryptionFormat
# Assume encryption_key_bytes is loaded correctly

# try:
#     encrypted_text = encrypt_data(encryption_key_bytes, "secret")
#     decrypted_text = decrypt_data(encryption_key_bytes, encrypted_text)
# except InvalidEncryptionKey as e:
#     # Critical configuration error, log and potentially halt
#     print(f"Crypto setup error: {e}")
# except InvalidEncryptionFormat as e:
#     # Problem with the data or key during decryption
#     print(f"Decryption failed: {e}. Data might be corrupt or tampered.")

General Error Handling Best Practices

  1. Be Specific: Catch the most specific exception type possible rather than generic Exception. This allows for more targeted error handling.
  2. Log Errors: Always log errors with sufficient context (e.g., relevant IDs, user information if appropriate, stack traces) to help with debugging. Use structured logging if possible.
  3. User-Friendly Messages: Do not expose raw exception messages or stack traces directly to end-users, especially for security-sensitive errors. Translate errors into user-friendly messages or generic error indicators (e.g., "An unexpected error occurred. Please try again later.").
  4. HTTP Status Codes: In web applications, map exceptions to appropriate HTTP status codes (e.g., 400 for bad input, 403 for forbidden, 404 for not found, 409 for conflict, 500 for server errors).
  5. Fail Fast/Securely: For critical errors like invalid encryption keys or missing handler registrations, it's often better for the application to fail fast during startup or indicate a severe issue rather than continuing in an unpredictable state.
  6. Unit of Work: When performing operations that involve multiple steps (e.g., database writes and event publishing), consider using a Unit of Work pattern to ensure atomicity. If an error occurs, roll back database changes to maintain consistency.
  7. Testing: Write tests that specifically trigger and verify the handling of these custom exceptions to ensure your error paths are working as expected.

By understanding the custom exceptions provided by castlecraft-engineer and following these best practices, you can build more resilient and secure applications.