Skip to content

Add DotflowError exception hierarchy for typed error handling #284

Description

@FernandoCelmer

Labels: enhancement, good first issue

Context

Today, exceptions raised inside the library inherit directly from Exception. Examples in dotflow/core/exception.py:

class MissingActionDecorator(Exception): ...
class ExecutionModeNotExist(Exception): ...
class NotCallableObject(Exception): ...

There is no shared root, so consumers cannot write except DotflowError to handle library-originated failures specifically. They must catch Exception and inspect the type, which is fragile and easy to get wrong.

TaskError (in the same file) is a serializer record (it captures exception_name, traceback, message for persistence). It is intentionally not an Exception subclass and should not be touched.

Concept

Introduce a single root, DotflowError, and a small hierarchy of typed exceptions for the common failure modes the engine can raise. Existing exceptions become subclasses of DotflowError. New exceptions are added for cases the library currently signals through generic Exception (retry exhaustion, task timeout, dependency cycles, input-fingerprint mismatch).

Proposed hierarchy

# dotflow/core/exception.py

class DotflowError(Exception):
    """Root of all library-raised exceptions."""

class RetryExhausted(DotflowError):
    def __init__(self, task_id, attempts, last_error):
        self.task_id = task_id
        self.attempts = attempts
        self.last_error = last_error
        super().__init__(
            f"Task {task_id} failed after {attempts} attempts"
        )

class TaskTimeout(DotflowError):
    def __init__(self, task_id, seconds):
        super().__init__(
            f"Task {task_id} exceeded timeout of {seconds}s"
        )

class CycleError(DotflowError):
    """Task graph contains a cycle."""

class InputChangedError(DotflowError):
    """initial_context fingerprint differs from stored checkpoint
    (see #281)."""

class CompensationFailed(DotflowError):
    """Saga rollback step failed (see #277)."""

# Existing exceptions become subclasses
class MissingActionDecorator(DotflowError): ...
class ExecutionModeNotExist(DotflowError): ...
class NotCallableObject(DotflowError): ...
class ImportModuleError(DotflowError): ...

Consumer benefit

try:
    workflow.start()
except RetryExhausted as e:
    logger.warning("retry exhausted on task %s", e.task_id)
except DotflowError:
    raise   # any other dotflow-originated error
except Exception:
    raise   # unrelated bugs in user code

Acceptance criteria

  • DotflowError exists at dotflow/core/exception.py
  • RetryExhausted, TaskTimeout, CycleError,
    InputChangedError, CompensationFailed defined
  • All existing custom exceptions inherit from DotflowError
  • TaskError serializer untouched
  • Test: assert isinstance(MissingActionDecorator(), DotflowError)
  • Docs: section in error-handling guide

Out of scope

  • Wiring the new exceptions into engine flow (RetryExhausted raised
    on exhaustion, etc.) is a follow-up. This issue introduces the hierarchy; engine integration is tracked separately.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions