Implementing a New Exception and Handler
This guide describes how to correctly introduce a new exception into the backend → worker → recovery → handler pipeline.
The critical architectural rule is:
Backend code raises exceptions. Worker catches them. RecoveryService routes them. Handler defines policy.
Exception Flow Overview
- Backend Layer
Raises domain exception.
- Worker Thread Boundary
Catches exception, wraps it as a
TaviError(if needed), publishesExceptionEvent.- RecoveryService
Routes exception to registered handler.
- Frontend (ErrorPresenter)
Defines user interaction and recovery policy.
Step 1 — Define the Exception
Classify by workflow semantics:
RecoverableError→ additional system workflow required.NonRecoverableError→ current workflow stops.
Example:
from tavi.meta.exception.nonrecoverable.base import NonRecoverableError
class InvalidConfigurationError(NonRecoverableError):
pass
Or:
from tavi.meta.exception.recoverable.base import RecoverableError
class AuthenticationExpiredError(RecoverableError):
pass
Exception classes should contain no behavior — only state.
Step 2 — Raise the Exception in Backend Code
Backend services raise domain exceptions normally:
if not config.is_valid():
raise InvalidConfigurationError(
message="Configuration is invalid.",
stack_trace="captured upstream or placeholder"
)
No UI logic should exist in backend code.
Step 3 — Worker Catches and Dispatches
The Worker acts as the thread boundary and central exception capture point.
Simplified flow:
try:
results = self.target(*self.args, **self.kwargs)
except RecoverableError as e:
...
except NonRecoverableError as e:
...
except Exception as e:
stack_trace = ...
self.event_broker.publish(
ExceptionEvent(e=NonRecoverableError(error_message, stack_trace))
)
Key responsibilities of Worker:
Prevent exceptions from escaping the thread
Capture stack trace context
Convert unexpected exceptions into domain-level
TaviErrorDispatch via
EventBroker
Important:
If backend code already raises a TaviError subtype,
the Worker may pass it through directly instead of wrapping it.
Step 4 — Register a Handler
Handlers currently are registered in the frontend orchestration layer:
recovery.register(InvalidConfigurationError, self.handle_invalid_config)
Rules:
Exact type matching
One handler per exception type
Handler defines outcome (fatal, retry, ignore, escalate)
Step 5 — Implement the Handler
Handlers define policy and UI behavior.
Example: NonRecoverable
def handle_invalid_config(self, ex: InvalidConfigurationError) -> None:
self.application_model.write_error_log(ex.stack_trace)
TaviMessageBox.critical(self.view, "Error", str(ex))
This stops the workflow and returns control to the user.
Example: Recoverable
def handle_auth_expired(self, ex: AuthenticationExpiredError) -> None:
if self.auth_service.refresh_token():
self.retry_original_action()
else:
self.escalate_to_fatal(ex)
Worker Contract
The Worker enforces:
All backend exceptions are captured
All errors become
TaviErrorinstancesAll errors enter the system through
ExceptionEventBackend never interacts directly with UI
This keeps thread boundaries clean and recovery centralized.
Design Guidelines
Raise exceptions only in backend logic.
Never perform UI operations inside backend services.
Let Worker normalize and dispatch errors.
Keep exception classes minimal.
Keep handlers focused and deterministic.
Always register new exception types explicitly.
Checklist
When adding a new exception:
[ ] Subclass correct base (Recoverable vs NonRecoverable)
[ ] Raise only in backend layer
[ ] Ensure Worker dispatches it correctly
[ ] Register handler in presenter
[ ] Define logging policy
[ ] Define escalation behavior
Architectural Summary
Backend expresses failure. Worker captures and dispatches. RecoveryService routes. Handler decides outcome.
This keeps responsibilities sharply separated and prevents cross-layer leakage.