Benefits of Strongly Typed Error Messages

How Resolute can work with custom defined exception types for various quality of life features like dropping sensitive information and localization.

Benefits of Strongly Typed Error Messages
Photo by Markus Winkler / Unsplash

String error messages scattered across the application codebase are a maintenance hazard. They duplicate text, drift out of sync, and give you no way to distinguish a validation failure from a runtime fault without inspecting the message itself. A professional alternative is to declare all error conditions once, as named class attributes, and let the type system do the rest. The so created error message cataloge can also serve as centralized collection of your business rules if combined with a thorough "always-valid" domain model.

class ValidatingError(ValueError, DomainError):
    pass

class ValidationErrors:
    email_must_contain_at = typed(ValidatingError) \
        .localize("de-DE", "Eine gültige Email muss ein '@' enthalten") \
        .localize("en-US", "A valid email address needs to contain '@' symbol")

    price_must_be_positive = typed(ValidatingError) \
        .localize("en-US", "Price must be a positive number") \
        .localize("de-DE", "Preis muss ein positive Zahl sein")

The validator in the User entity raises ValidationErrors.email_must_contain_at instead of a raw string. There is one authoritative definition and no way to get the text wrong at the call site.

@field_validator("email")
def validate_email(cls, value: str) -> str:
    if not "@" in value:
        raise ValidationErrors.email_must_contain_at
    return value

At the application boundary you can now filter errors by type rather than by parsing message strings. The Resolute result type — which accepts both exception subtypes and plain strings as error carriers — supports this directly:

if result.has_errors:
    raise HTTPException(status_code=400, detail=result.remove_errors_except_of_type([ValidatingError]).concat_errors())

This lets you expose validation failures to callers while stripping internal errors that should not leave the service boundary.

ValidatingError inherits from both DomainError and ValueError. That second base class matters when Pydantic models are used directly as request bodies: FastAPI's exception handlers catch ValueError subtypes during request parsing, so your typed domain errors plug into the framework's error handling pipeline without any adapter code.

resolute
Implementation of the Result pattern similar to C# ErrorOr package