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), publishes ExceptionEvent.

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 TaviError

  • Dispatch 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 TaviError instances

  • All errors enter the system through ExceptionEvent

  • Backend never interacts directly with UI

This keeps thread boundaries clean and recovery centralized.

Design Guidelines

  1. Raise exceptions only in backend logic.

  2. Never perform UI operations inside backend services.

  3. Let Worker normalize and dispatch errors.

  4. Keep exception classes minimal.

  5. Keep handlers focused and deterministic.

  6. 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.