better-py

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 result
  • Failure(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()    # True

Extracting 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()      # None

The 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)     # 0

Getting 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()                   # None

Use 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()    # Nothing

Use 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 Result instead)
  • You want type-safe error handling (use Result instead)
  • Exceptions are the right choice (let them propagate)

Comparison with Result

FeatureTryResult
Error typeExceptionAny type
CreationAutomatic (catches exceptions)Manual
Type safetyLess type-safeMore type-safe
Use caseWrapping exception codeNew 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.

See Also

  • Result - For type-safe error handling
  • Maybe - For optional values
  • IO - For side effect management

On this page