Batu Lab NotesPractical developer guides

Preserve the original exception with raise from

By Batu · English technical notes

Also published in our primary archive.

To preserve the original exception with raise from, catch the conversion error at the configuration boundary and raise the domain-specific error from that caught instance. The example deliberately converts "x" with int(). The first wrapper raises a replacement error without from; Python records contextual information while handling it, but its explicit __cause__ is None. That makes the intended causal relationship unavailable to callers that inspect the direct cause.

The corrected wrapper catches ValueError as error and uses raise ConfigValueError(...) from error. The final assertions check both branches: the old wrapper has no explicit cause, while the corrected error has a ValueError cause whose message is the failed integer input. The printed result is a compact, deterministic representation of the relevant trace chain rather than a machine-dependent rendered traceback.

Keep this boundary narrow: only translate the ValueError emitted by the conversion. Catching Exception here would incorrectly label unrelated programming or runtime failures as configuration errors. raise ... from ... is available in Python 3.0 and later. Python’s tutorial documents explicit exception chaining and shows that from marks the original exception as the direct cause. Python error and exception tutorial

AI assistance disclosure: This synthetic example and explanation were prepared with AI assistance and should be adapted to the application’s own error contract.

class ConfigValueError(ValueError):
    pass


def wrap_without_cause(raw_value):
    try:
        return int(raw_value)
    except ValueError:
        raise ConfigValueError("config value must be an integer")


def wrap_with_cause(raw_value):
    try:
        return int(raw_value)
    except ValueError as error:
        raise ConfigValueError("config value must be an integer") from error


try:
    wrap_without_cause("x")
except ConfigValueError as error:
    assert error.__cause__ is None
    print("replacement explicit cause: None")

try:
    wrap_with_cause("x")
except ConfigValueError as error:
    assert isinstance(error.__cause__, ValueError)
    assert str(error.__cause__) == "invalid literal for int() with base 10: 'x'"
    print(f"trace chain: {type(error).__name__} caused by {type(error.__cause__).__name__}")
replacement explicit cause: None
trace chain: ConfigValueError caused by ValueError