Error Handling
Relevant source files
The following files were used as context for generating this wiki page:The error handling system in SOFL is designed to provide a structured, robust, and user-friendly way to manage exceptions that occur throughout the application. It distinguishes between internal exceptions and those that need to be presented to the user, providing mechanisms for error aggregation, retrying failed operations, and displaying clear messages in the user interface.
The system is built around a few core components: the FriendlyError custom exception for user-facing messages, the ErrorProducer abstract class for collecting errors, and various implementation patterns in managers, importers, and UI components. This ensures that errors from background tasks, such as game importing or cover fetching, are handled gracefully without crashing the application and are communicated effectively to the user.
Core Components
The foundation of the error handling architecture is composed of two key classes that define how errors are structured and collected.
FriendlyError
FriendlyError is a custom exception class designed to wrap system-level exceptions and attach user-friendly information to them. This allows the application to catch a generic Exception but still present a specific, helpful message to the user.
Key characteristics:
- It inherits from Python's base
Exceptionclass. - It is initialized with a
titleandsubtitle, which are intended for display in UI dialogs. - It stores the original exception in its
__cause__attribute, preserving the original stack trace for logging and debugging purposes.
The class diagram below shows its simple structure.
Sources: sofl/errors/friendly_error.py
ErrorProducer
ErrorProducer is an abstract class that provides a standardized interface for any component that needs to generate and aggregate errors. Classes that inherit from ErrorProducer can collect multiple exceptions over their lifecycle, which can then be retrieved by another part of the system.
The class provides two main methods:
report_error(error: Exception): Adds an exception to an internal_errorslist.collect_errors() -> list[Exception]: Returns the list of collected errors and clears the internal list.
This pattern is used by Manager and Importer classes to gather errors during background processing.
Sources: sofl/errors/error_producer.py
Error Handling in Managers
The Manager base class, found in sofl/store/managers/manager.py, is a prime example of the error handling system in action. It inherits from ErrorProducer and implements a sophisticated, configurable error handling and retry mechanism for its subclasses.
Sources: sofl/store/managers/manager.py
Manager Execution Flow
The run method of the Manager class orchestrates the execution of the manager's main logic (main method) and handles any exceptions that arise. This flow includes logic for retrying, ignoring, or reporting errors based on the manager's configuration.
The following diagram illustrates the error handling logic within the Manager.run() method.
Configuration
Subclasses of Manager can configure the error handling behavior by defining several class attributes.
| Attribute | Type | Description |
|---|---|---|
retryable_on | Container[type[Exception]] | A tuple of exception types that should trigger a retry attempt. |
continue_on | Container[type[Exception]] | A tuple of exception types that should be silently ignored. |
retry_delay | int | The number of seconds to wait between retry attempts. Default is 3. |
max_tries | int | The maximum number of attempts for a single operation. Default is 3. |
Sources: sofl/store/managers/manager.py:31-35
Error Handling in Importers
The game import process is a complex, multi-step operation that can fail in various ways. The Importer class orchestrates this process and aggregates errors from all sources and managers.
Sources: sofl/importer/importer.py
Error Aggregation
The Importer class inherits from ErrorProducer and serves as a central collection point for all errors that occur during an import session.
- The
run()method initiates the import, and each game source is scanned in a separate thread. - Within
source_task_thread_func, any exception raised while iterating over a source is caught and reported viaself.report_error(error). - After all sources and game-processing pipelines are finished, the
finish_import()method is called. - This method calls
create_error_dialog(), which gathers all errors from theImporteritself and from every registeredManagerinshared.store.managers. - The collected errors are filtered to find
FriendlyErrorinstances, which are then displayed to the user in a single dialog.
Sources: sofl/importer/importer.py:173-206
The following sequence diagram shows how an error from a source is captured and eventually displayed.
User-Facing Error Display
Errors are ultimately communicated to the user through different UI patterns, depending on the context and severity.
Modal Dialogs (create_dialog)
For immediate, blocking issues like input validation or critical failures, the application uses a simple modal dialog created by the create_dialog utility. This is common in UI components like DetailsDialog.
For example, when applying changes to a game, if the name or executable path is empty, a dialog is shown immediately.
# From sofl/details_dialog.py
if final_name == "":
create_dialog(
self, _("Couldn't Add Game"), _("Game title cannot be empty.")
)
return
Sources: sofl/details_dialog.py:214-219
This pattern is also used to display FriendlyError messages from asynchronous operations, such as fetching a game cover from SteamGridDB. The callback function collects the error from the manager and uses its title and subtitle to populate the dialog.
# From sofl/details_dialog.py
def update_cover_callback(self, manager: SgdbManager) -> None:
# ...
for error in manager.collect_errors():
if isinstance(error, FriendlyError):
create_dialog(
shared.win,
error.title,
error.subtitle,
"open_preferences",
_("Preferences"),
)
Sources: sofl/details_dialog.py:348-359
Contextual Warnings (Popover)
For non-critical issues, such as a missing game installation directory, a less intrusive pattern is used. In the SOFLPreferences window, if a source's location cannot be resolved (UnresolvableLocationError), a warning icon with a Gtk.Popover is added directly to the relevant settings row. This informs the user of the problem without interrupting their workflow.
This diagram shows the logic for displaying a contextual warning.
Sources: sofl/preferences.py:273-324
Summary
SOFL's error handling system effectively decouples error generation from error presentation. It uses the ErrorProducer pattern for robust error collection in background tasks, while the FriendlyError class ensures that messages shown to the user are clear and helpful. The retry mechanism in the Manager class adds resilience to network-dependent operations. Finally, the UI employs both modal dialogs and contextual popovers to communicate issues appropriately, creating a stable and user-friendly experience.