How to Convert Exception-Throwing Functions to Result¶
This guide shows how to use Result.catch to turn an ordinary function that
raises exceptions into one that returns a Result — without writing a
try/except block yourself. It also covers how to recover the exact line that
caused a failure from the captured exception’s traceback.
Prerequisites¶
Familiarity with
Result.Success/Result.Failureand the|operatorIf you are new to
Result, work through the Build a User Registration System with Result tutorial first, and see How to Handle Errors Without Exceptions for the manual try/except approach
Wrap a throwing function with a single decorator¶
Result.catch is a decorator factory. Pass it the exception type you want to
intercept; it returns a decorator that wraps the function so a normal return
value becomes a Success and a matching exception becomes a Failure:
from katharos.types import Result
@Result.catch(ValueError)
def parse_int(s: str) -> int:
return int(s)
print(parse_int("42")) # Success(42)
print(parse_int("bad")) # Failure(ValueError("invalid literal for int() with base 10: 'bad'"))
This replaces the manual boundary code you would otherwise write:
def parse_int(s: str) -> Result[ValueError, int]:
try:
return Result.Success(int(s))
except ValueError as e:
return Result.Failure(e)
The decorated function keeps its name and docstring (catch uses
functools.wraps internally), so it behaves like the original everywhere else.
Catch only the declared type; let others propagate¶
Only the exception type you name (and its subclasses) is converted to a
Failure. Any other exception propagates unchanged, exactly as it would
without the decorator:
@Result.catch(ValueError)
def risky(x: int) -> int:
if x < 0:
raise TypeError("negative")
return x
print(risky(1)) # Success(1)
risky(-1) # raises TypeError — NOT caught, not wrapped in a Result
This is deliberate: catch narrows exception handling to the failures you
expect, while genuine programming errors still surface as exceptions.
Cover a family of exceptions with a base class¶
Because subclasses are intercepted too, naming a base exception catches all of
its descendants. For example, OSError covers FileNotFoundError,
PermissionError, and the rest of the OS error hierarchy:
@Result.catch(OSError)
def read_file(path: str) -> str:
with open(path) as f:
return f.read()
read_file("/nonexistent")
# Failure(FileNotFoundError(2, 'No such file or directory'))
Chain the result with the rest of your pipeline¶
A function decorated with catch returns an ordinary Result, so it
composes with fmap, bind, and the | operator like any other. A
failure short-circuits the rest of the chain:
@Result.catch(ValueError)
def parse_int(s: str) -> int:
return int(s)
print(parse_int("10").fmap(lambda n: n * 2)) # Success(20)
print(parse_int("bad").fmap(lambda n: n * 2)) # Failure(ValueError(...)) — lambda never runs
@Result.catch(ZeroDivisionError)
def reciprocal(n: int) -> float:
return 1 / n
# parse, then divide — the first failure wins
print(parse_int("4") | reciprocal) # Success(0.25)
print(parse_int("0") | reciprocal) # Failure(ZeroDivisionError('division by zero'))
Recover the line that caused the failure¶
catch stores the original exception instance in the Failure, and
Python attaches the traceback to that instance when it is raised. Because the
exception is captured rather than re-raised, its __traceback__ is preserved —
so you can inspect exactly where the failure originated:
import traceback
from katharos.types import Result
@Result.catch(ValueError)
def parse_int(s: str) -> int:
return int(s)
result = parse_int("bad")
if result.is_failure():
# Print the full traceback, including the failing line inside parse_int
traceback.print_exception(result.error)
# Or format it as a list of frames to log or attach to a report
frames = traceback.format_tb(result.error.__traceback__)
print("".join(frames))
This is the key advantage over patterns that discard the stack: you get errors-as-values and keep the debugging information you would have had from a normal exception.
See also¶
How to Handle Errors Without Exceptions — handle errors with manual
try/exceptboundaries and convert betweenMaybeandResultErrors as Values: catch and Traceback Preservation — why
catchworks this way and why preserving the traceback matters