Try Monad
Handle exceptions as values with Success and Failure
The Try monad represents operations that can succeed with a value or fail with an exception. It's a way to handle exceptions as values rather than using try/except blocks, making error handling explicit and composable.
Try[T] wraps operations that might raise exceptions:
Success(value)- Contains a successful resultFailure(exception)- Contains an exception
Creating & Checking
The Try.of method executes a function and catches any exceptions, wrapping the result in Success or Failure.
from better_py import Try
# Success case
Try.of(lambda: 42) # Success(42)
# Failure case
Try.of(lambda: int("abc")) # Failure(ValueError)
# With exception
Try.of(lambda: 1 / 0) # Failure(ZeroDivisionError)You can also create Success or Failure directly:
Try.success(42) # Success(42)
Try.failure(ValueError("Invalid")) # Failure(ValueError('Invalid'))Check variants with is_success and is_failure:
success = Try.success(42)
failure = Try.failure(ValueError("bad"))
success.is_success() # True
failure.is_failure() # TrueExtracting Values
Getting Values or Defaults
The get method returns the success value or None if Failure.
Try.success(42).get() # 42
Try.failure(ValueError("bad")).get() # NoneThe get_or_else method returns the success value or a default.
Try.success(42).get_or_else(0) # 42
Try.failure(ValueError("bad")).get_or_else(0) # 0Getting Exceptions
The get_exception method returns the exception if Failure, or None if Success.
Try.failure(ValueError("bad")).get_exception() # ValueError('bad')
Try.success(42).get_exception() # NoneUse this when you need to inspect or log the exception that occurred.
Transforming Values
The map method applies a function to the Success value. If the function raises an exception, it's caught and wrapped in Failure.
from better_py import Try
Try.success(5).map(lambda x: x * 2) # Success(10)
# Exceptions are caught
Try.success(5).map(lambda x: int("abc")) # Failure(ValueError)
# Failure short-circuits
Try.failure(ValueError("bad")).map(lambda x: x * 2) # Failure(ValueError)This is safer than manual exception handling, as exceptions within map are automatically caught and converted to Failure.
Chaining Operations
The flat_map method chains operations that return Try. If any step returns Failure, the rest of the chain is skipped.
from better_py import Try
def divide(x: float) -> Try[float]:
return Try.of(lambda: 10 / x)
Try.success(2).flat_map(divide) # Success(5.0)
Try.success(0).flat_map(divide) # Failure(ZeroDivisionError)
Try.failure(ValueError("bad")).flat_map(divide) # Failure(ValueError)Use flat_map for sequential operations where each step depends on the previous one. Exceptions in any step automatically convert to Failure.
Recovering from Failure
The recover method transforms Failure into Success using a recovery function. If the recovery function raises an exception, that becomes the new Failure.
from better_py import Try
# Recover from failure
Try.failure(ValueError("bad")).recover(lambda e: 0) # Success(0)
# No effect on success
Try.success(42).recover(lambda e: 0) # Success(42)
# Recovery can also fail
Try.failure(ValueError("bad")).recover(lambda e: int("abc")) # Failure(ValueError)Use recover to provide default values or fallback logic when operations fail.
Handling Both Cases
The fold method transforms both Success and Failure into a common result type. You provide two functions—one for each case—and fold applies the appropriate one.
from better_py import Try
def describe(result: Try) -> str:
return result.fold(
on_failure=lambda e: f"Error: {e}",
on_success=lambda v: f"Value: {v}"
)
describe(Try.success(42)) # "Value: 42"
describe(Try.failure(ValueError("bad"))) # "Error: bad"Fold is the ultimate pattern match for Try, letting you handle both cases in one expression.
Converting to Maybe
The to_option method converts Try to Maybe. Success becomes Some, while Failure becomes Nothing.
from better_py import Try, Some, Nothing
Try.success(42).to_option() # Some(42)
Try.failure(ValueError("bad")).to_option() # NothingUse this when integrating with code that uses Maybe, discarding the exception information.
Real-World Pattern: Safe File Operations
from better_py import Try
def read_file(path: str) -> Try[str]:
return Try.of(lambda: open(path).read())
def parse_json(content: str) -> Try[dict]:
import json
return Try.of(lambda: json.loads(content))
# Chain operations
result = (read_file("data.json")
.flat_map(parse_json)) # Success({...}) or Failure(...)This pattern shows Try's power: file operations and JSON parsing can both fail, but Try handles both cases gracefully without nested try/except blocks.
When to Use
Use Try when:
- Working with exception-throwing code
- You want to handle exceptions as values
- You need to catch and process exceptions
- Wrapping legacy code with exceptions
Don't use Try when:
- You have control over the code (use
Resultinstead) - You want type-safe error handling (use
Resultinstead) - Exceptions are the right choice (let them propagate)
Comparison with Result
| Feature | Try | Result |
|---|---|---|
| Error type | Exception | Any type |
| Creation | Automatic (catches exceptions) | Manual |
| Type safety | Less type-safe | More type-safe |
| Use case | Wrapping exception code | New code with explicit errors |
Rule of thumb: Use Try when wrapping existing code that throws exceptions. Use Result for new code where you control error creation.