api_error_processor
api_error_processor builds a decorator for public API exception normalization.
It is used at public boundaries of library components, where raw internal exceptions should not leak directly to callers.
Why it exists
Library internals may raise ordinary Python exceptions:
ValueError
TypeError
AssertionError
KeyError
ZeroDivisionError
Those exceptions are often useful inside the implementation, but they are a poor public API surface. If they leak directly, callers become coupled to internal implementation details.
api_error_processor turns that boundary into a controlled error surface.
Declared public errors pass through unchanged. Unexpected internal exceptions are
wrapped into a configured RuntimeExtendedError subclass.
Error categories
The decorator separates exceptions into three categories.
Cancellation
asyncio.CancelledError is always re-raised unchanged.
Cancellation is control flow, not an application failure. It must not be wrapped.
import asyncio
try:
...
except asyncio.CancelledError:
raise
Passthrough errors
Errors listed in passthrough_error_types are re-raised unchanged.
These are expected, declared API errors that callers may handle directly.
from mvx.common.helpers import api_error_processor
from mvx.common.errors import RuntimeExtendedError
class ApiInputError(Exception):
...
class ServiceUnexpectedError(RuntimeExtendedError):
...
public_api = api_error_processor(
passthrough_error_types=(ApiInputError,),
raise_error_type=ServiceUnexpectedError,
)
If the wrapped function raises ApiInputError, the decorator does not modify it.
Existing RuntimeExtendedError
If the exception is already a RuntimeExtendedError, it is also re-raised
unchanged.
Before re-raising, the decorator fills missing source metadata:
module
qualname
This keeps existing structured errors intact while making logs more informative.
Unexpected errors
Any other Exception is treated as an unexpected internal failure.
The decorator wraps it into raise_error_type:
raise_error_type(
message=f"runtime unexpected error: {exc}",
module=module,
qualname=qualname,
cause=exc,
)
The original exception is preserved as the cause.
Constructor contract
raise_error_type must be a RuntimeExtendedError subclass.
It should support this constructor shape:
error = raise_error_type(
message="runtime unexpected error: ...",
module="...",
qualname="...",
cause=original_exception,
)
This is the normal path.
The decorator also contains a defensive fallback for misconfigured error classes. If the full constructor fails, it tries to instantiate the error with only the message and then assigns the remaining fields manually.
This fallback exists to avoid double-faulting while processing an exception. In a well-formed package, the primary constructor should always succeed.
Sync and async support
The decorator supports both synchronous and asynchronous callables.
Synchronous function:
@public_api
def compute(value: int) -> int:
return value * 2
Asynchronous function:
@public_api
async def fetch(value: int) -> int:
return value
Coroutine detection uses inspect.unwrap() before checking the callable. This
allows the decorator to work correctly when the function has already been wrapped
by other decorators.
Basic usage
from mvx.common.errors import RuntimeExtendedError, RuntimeUnexpectedError
from mvx.common.helpers import api_error_processor
class ServiceError(RuntimeExtendedError):
pass
class ServiceUnexpectedError(ServiceError, RuntimeUnexpectedError):
pass
class ApiInputError(ValueError):
pass
public_api = api_error_processor(
passthrough_error_types=(ApiInputError,),
raise_error_type=ServiceUnexpectedError,
)
Synchronous example
from mvx.common.errors import RuntimeExtendedError, RuntimeUnexpectedError
from mvx.common.helpers import api_error_processor
class ServiceError(RuntimeExtendedError):
pass
class ServiceUnexpectedError(ServiceError, RuntimeUnexpectedError):
pass
class ApiInputError(ValueError):
pass
public_api = api_error_processor(
passthrough_error_types=(ApiInputError,),
raise_error_type=ServiceUnexpectedError,
)
class ExampleService:
@public_api
def compute(self, value: int) -> int:
if value < 0:
raise ApiInputError("value must be non-negative")
if value == 13:
raise AssertionError("unexpected internal invariant")
return value * 2
Declared API errors pass through:
service = ExampleService()
try:
service.compute(-1)
except ApiInputError:
handled = True
Unexpected errors are wrapped:
try:
service.compute(13)
except ServiceUnexpectedError as exc:
payload = exc.to_log_payload()
Example payload:
payload = {
"module": "__main__",
"qualname": "ExampleService.compute",
"kind": "ServiceUnexpectedError",
"message": "runtime unexpected error: unexpected internal invariant",
"details": {},
"cause": {
"kind": "AssertionError",
"message": "unexpected internal invariant",
},
}
Asynchronous example
from mvx.common.errors import RuntimeExtendedError, RuntimeUnexpectedError
from mvx.common.helpers import api_error_processor
class ServiceError(RuntimeExtendedError):
pass
class ServiceUnexpectedError(ServiceError, RuntimeUnexpectedError):
pass
class ApiInputError(ValueError):
pass
public_api = api_error_processor(
passthrough_error_types=(ApiInputError,),
raise_error_type=ServiceUnexpectedError,
)
class ExampleService:
@public_api
async def fetch(self, value: int) -> int:
if value == 0:
raise ZeroDivisionError("division by zero")
return 10 // value
The async path uses the same exception policy:
service = ExampleService()
try:
result = await service.fetch(0)
except ServiceUnexpectedError as exc:
payload = exc.to_log_payload()
Public API pattern
The intended pattern is to create one decorator instance per component or module:
public_api = api_error_processor(
passthrough_error_types=(ServiceError,),
raise_error_type=ServiceUnexpectedError,
)
Then apply it to public methods:
class Service:
@public_api
def method(self) -> None:
...
This keeps public error behavior consistent across the component.
Design rule
Use api_error_processor at public API boundaries.
Do not use it deep inside implementation code. Internal layers should raise specific errors naturally. The public boundary is where those errors are either allowed to pass through or are mapped into the public unexpected-error type.
The decorator should make the public surface stable without hiding useful diagnostic information.
API
- mvx.common.helpers.api_error_processor(*, passthrough_error_types, raise_error_type)
Build a decorator for public API exception normalization.
- Parameters:
passthrough_error_types (
tuple[type[Exception],...]) – Exception types that must pass through unchanged.raise_error_type (
type[RuntimeExtendedError]) – RuntimeExtendedError subclass used to wrap unexpected exceptions.
- Return type:
Callable[[TypeVar(F, bound=Callable[...,Any])],TypeVar(F, bound=Callable[...,Any])]- Returns:
Decorator that applies the public API error policy to sync or async callables.