Skip to content

typed-error-reasons

Review joined exception strings for fixed reason identities versus dynamic context.

Why

Fixed reason identities benefit from stable codes for API clients, UI formatting, telemetry, and exhaustive handling. Open-ended values such as file paths remain typed context, not an enum domain.

Fix

If the strings are fixed reason identities, use a StrEnum code with typed context. Keep dynamic paths and messages as typed context; format presentation separately.

Examples

Before — flagged Review a contextual list without converting paths to enum values
app/errors.py
class MissingFilesError(Exception):
def __init__(self, paths: list[str]) -> None:
super().__init__(", ".join(paths))
After — preferred Keep arbitrary paths as typed context without an enum
app/errors.py
class MissingFilesError(Exception):
def __init__(self, paths: list[str]) -> None:
self.paths = paths
super().__init__("Files are missing")
def render_error(error: MissingFilesError) -> str:
return ", ".join(error.paths)
Before — flagged An exception renders raw reason strings
app/errors.py
class DomainError(Exception): ...
class IncompleteError(DomainError):
def __init__(self, reasons: list[str]) -> None:
self.reasons = reasons
super().__init__(f"Incomplete: {'; '.join(reasons)}")
After — preferred An exception carries nominal reason records
app/errors.py
from dataclasses import dataclass
from enum import StrEnum
class ReasonCode(StrEnum):
MISSING_URL = "missing_url"
@dataclass(frozen=True)
class Reason:
code: ReasonCode
field: str | None = None
class DomainError(Exception): ...
class IncompleteError(DomainError):
def __init__(self, reasons: list[Reason]) -> None:
self.reasons = reasons
super().__init__("Integration is incomplete")