StructuredError

StructuredError is the base exception class for MVX errors that need to carry structured diagnostic context.

It extends the built-in Exception with explicit fields for:

  • a human-readable message;

  • structured diagnostic details;

  • an optional underlying cause;

  • a stable logging payload.

Why it exists

Plain Python exceptions are good for control flow and stack traces, but they are not enough for library-level error handling.

A usual exception often gives only this:

ValueError: invalid value

That is readable for a developer, but weak for logs, telemetry, tests and API boundaries. It does not reliably answer:

  • what kind of domain error happened;

  • which object, operation or state caused it;

  • whether there was an underlying low-level exception;

  • what payload should be sent to structured logging.

StructuredError solves this by making error context explicit.

Instead of hiding everything inside a formatted string, it keeps diagnostic data in a normal dictionary:

from mvx.common.errors import StructuredError

request_id = "req-123"

raise StructuredError(
    message="Failed to process request",
    details={
        "request_id": request_id,
        "operation": "bind",
        "state": "READY",
    },
)

This gives the code two different representations of the same error:

  • str(error) — readable text for humans;

  • error.to_log_payload() — stable structured data for logs.

Basic usage

from mvx.common.errors import StructuredError


def load_entity(entity_id: str) -> None:
    raise StructuredError(
        message="Failed to load entity",
        details={"entity_id": entity_id},
    )

String representation:

StructuredError: Failed to load entity | details={'entity_id': 'abc-123'}

Log payload:

log_payload={
    "kind": "StructuredError",
    "message": "Failed to load entity",
    "details": {
        "entity_id": "abc-123",
    },
}

Wrapping an underlying exception

StructuredError can keep an original exception as its cause.

from mvx.common.errors import StructuredError


def parse_port(raw_port: str) -> int:
    try:
        return int(raw_port)
    except ValueError as exc:
        raise StructuredError(
            message="Failed to parse port",
            details={"raw_port": raw_port},
            cause=exc,
        ) from exc

The cause is stored on the error object and is also exposed through Python’s standard exception chaining mechanism via __cause__.

The logging payload will include a compact representation of the cause:

log_payload ={
    "kind": "StructuredError",
    "message": "Failed to parse port",
    "details": {
        "raw_port": "abc",
    },
    "cause": {
        "kind": "ValueError",
        "message": "invalid literal for int() with base 10: 'abc'",
    },
}

Details

details is intended for small, non-secret diagnostic context.

Good examples:

details={
    "operation": "connect",
    "state": "DISCONNECTED",
    "attempt": 1,
}

Bad examples:

details={
    "password": password,
    "token": access_token,
    "full_payload": huge_payload,
}

Do not put secrets, credentials, private keys, tokens or large payloads into details.

The constructor copies the incoming mapping into a plain dict. This prevents accidental mutation of the original object after the error has been created.

Adding details later

Sometimes the code creates an error at one layer and adds context at another layer before raising or logging it.

StructuredError supports a fluent style for that:

from mvx.common.errors import StructuredError

error = StructuredError(message="Operation failed")

error.with_detail("operation", "search")
error.with_detail("state", "READY")

raise error

Multiple details can be merged at once:

from mvx.common.errors import StructuredError

raise StructuredError(message="Operation failed").with_details(
    {
        "operation": "search",
        "state": "READY",
    }
)

Both helpers mutate the error object and return self.

Logging

Use to_log_payload() when the error needs to be passed to structured logging.

from mvx.common.errors import StructuredError

try:
    ...
except StructuredError as exc:
    logger.error(
        "operation.failed",
        extra={"error": exc.to_log_payload()},
    )

The payload shape is intentionally stable:

log_payload ={
    "kind": "<ConcreteErrorClassName>",
    "message": "<message>",
    "details": {...},
    "cause": {
        "kind": "<CauseClassName>",
        "message": "<cause message>",
    },
}

The cause field is included only when an underlying exception exists.

Design rule

StructuredError should not become a dumping ground for arbitrary data.

Its job is narrow:

  • preserve a clear human message;

  • carry small structured context;

  • keep the original cause when needed;

  • provide a stable logging representation.

Business-specific meaning should live in subclasses.

For example:

StructuredError
├── ReasonedError
└── InvalidFunctionArgumentError

StructuredError gives the common structure. Subclasses define the specific semantics.

API

class mvx.common.errors.StructuredError(*, message, details=None, cause=None)

Bases: Exception

Base exception class for errors with structured diagnostic context.

Parameters:
  • message (str) – Human-readable error message.

  • details (Optional[Mapping[str, Any]]) – Optional log-friendly diagnostic context.

  • cause (Optional[Exception]) – Optional underlying exception.

with_detail(key, value)

Add or replace one detail entry.

Parameters:
  • key (str) – Detail key.

  • value (Any) – Detail value.

Return type:

Self

Returns:

This error instance.

with_details(extra)

Merge multiple detail entries.

Parameters:

extra (Mapping[str, Any]) – Detail entries to merge.

Return type:

Self

Returns:

This error instance.

to_log_payload()

Return a stable dictionary representation for structured logging.

Return type:

dict[str, Any]

Returns:

Log-friendly error payload.