Result Monad
Explicit error handling with Ok and Error variants
The Result monad represents operations that can succeed (Ok) or fail (Error). It's a type-safe alternative to exceptions for error handling.
Result[T, E] has two variants:
Ok(value)- Contains a success value of typeTError(error)- Contains an error of typeE
Creating & Checking
Create Result values using Ok() and Error() constructors. Use is_ok() and is_error() to check which variant you have.
from better_py import Ok, Error
# Create values
success = Ok(42)
failure = Error("Something went wrong")
# Check variants
success.is_ok() # True
success.is_error() # False
failure.is_ok() # False
failure.is_error() # TrueYou can also use the Result class methods for creating values:
from better_py import Result
Result.ok(42) # Ok(42)
Result.error("Something failed") # Error("Something failed")
Result.from_value(42) # Ok(42)Extracting Values
The unwrap method returns the success value for Ok or raises an error for Error. Use unwrap_or and unwrap_or_else to provide defaults. Use unwrap_error to extract the error value.
from better_py import Ok, Error
Ok(42).unwrap() # 42
Error("failed").unwrap() # Raises ValueError
Ok(42).unwrap_or(0) # 42
Error("failed").unwrap_or(0) # 0
Ok(42).unwrap_or_else(lambda: expensive_computation()) # 42
Error("failed").unwrap_or_else(lambda: expensive_computation()) # Computation called
Error("failed").unwrap_error() # "failed"
Ok(42).unwrap_error() # Raises ValueErrorUse unwrap when you're certain the result is successful. Use unwrap_or for simple defaults. Use unwrap_or_else for expensive default computation. Use unwrap_error to access error information.
Transforming Values
The map method applies a function only to Ok, leaving Error unchanged. The map_error method applies a function only to Error, leaving Ok unchanged.
from better_py import Ok, Error
Ok(5).map(lambda x: x * 2) # Ok(10)
Error("bad").map(lambda x: x * 2) # Error("bad")
Error("bad").map_error(str.upper) # Error("BAD")
Ok(42).map_error(str.upper) # Ok(42)When transforming, the opposite variant passes through unchanged. This short-circuiting behavior is key to Result's utility in error handling.
Chaining Operations
The flat_map method chains operations that return Result. If any step returns Error, the rest of the chain is skipped—errors propagate automatically.
from better_py import Ok, Error
def validate_positive(x: int):
return Ok(x) if x > 0 else Error("Must be positive")
def validate_non_zero(x: int):
return Ok(x) if x != 0 else Error("Must be non-zero")
def divide(a: int, b: int):
return (validate_positive(a)
.flat_map(lambda _: validate_non_zero(b))
.map(lambda _: a / b))
divide(10, 2) # Ok(5.0)
divide(10, 0) # Error("Must be non-zero")
divide(-5, 2) # Error("Must be positive")This replaces nested if/else chains with a flat, readable pipeline. Each step receives the previous Ok value, or short-circuits on the first Error.
The methods bind and and_then are aliases for flat_map—use whichever reads best in your code.
Combining Results
The zip method combines multiple Result values into a tuple. If any value is Error, zip returns that Error immediately.
from better_py import Result, Ok, Error
Result.zip(Ok(1), Ok(2), Ok(3)) # Ok((1, 2, 3))
Result.zip(Ok(1), Error("bad"), Ok(3)) # Error("bad")Use zip when you need all values to proceed—like validating multiple fields where any failure should reject the entire operation.
The ap method applies a function wrapped in a Result to a value wrapped in a Result. Both must be Ok for the operation to succeed.
from better_py import Ok
add_one = Ok(lambda x: x + 1)
value = Ok(5)
add_one.ap(value) # Ok(6)
Error("bad").ap(value) # Error("bad")
add_one.ap(Error("bad")) # Error("bad")The lift2 and lift3 methods transform regular functions into functions that work with Result values.
from better_py import Result, Ok
def add(x, y):
return x + y
Result.lift2(add, Ok(5), Ok(10)) # Ok(15)
Result.lift2(add, Ok(5), Error("bad")) # Error("bad")Use these when you have existing functions that don't know about Result, and you want to use them with Result values without rewriting the functions.
Fallback Values
The or_else method returns this Result if it's Ok, or another Result if this one is Error. This is useful for chaining fallback operations.
from better_py import Ok, Error
Ok(5).or_else(Ok(10)) # Ok(5)
Error("bad").or_else(Ok(10)) # Ok(10)
# Chain fallbacks
result = (get_from_cache(key)
.or_else(get_from_database(key))
.or_else(get_from_api(key)))Use or_else when you want to try multiple sources in order, falling back to the next if the previous returns Error.
Pattern Matching
The fold method handles both cases and returns a non-Result result. You provide two functions—one for each case—and fold applies the appropriate one.
from better_py import Ok, Error
def describe(result):
return result.fold(
on_ok=lambda value: f"Success: {value}",
on_error=lambda err: f"Error: {err}"
)
describe(Ok(42)) # "Success: 42"
describe(Error("failed")) # "Error: failed"fold is the ultimate pattern match for Result—it's the only operation that lets you handle both cases in one expression and return a non-Result result.
Real-World Pattern: Validation Chain
from better_py import Ok, Error
def validate_age(age: int):
if age < 0:
return Error("Age cannot be negative")
if age > 150:
return Error("Age is unrealistic")
return Ok(age)
def validate_email(email: str):
if "@" not in email:
return Error("Invalid email format")
return Ok(email)
def create_user(age: int, email: str):
return (validate_age(age)
.flat_map(lambda _: validate_email(email))
.map(lambda _: {"age": age, "email": email}))
create_user(25, "user@example.com") # Ok({"age": 25, "email": "user@example.com"})
create_user(-5, "user@example.com") # Error("Age cannot be negative")
create_user(25, "invalid") # Error("Invalid email format")This pattern shows Result's strength: each validation step can return Error with a message, and the entire pipeline short-circuits on the first failure. No intermediate variables or nested conditionals needed.
Real-World Pattern: API Error Handling
from better_py import Ok, Error
import requests
class APIError:
def __init__(self, message: str, status_code: int):
self.message = message
self.status_code = status_code
def fetch_url(url: str):
"""Fetch URL and return Result[str, APIError]."""
try:
response = requests.get(url, timeout=5)
if response.status_code == 200:
return Ok(response.text)
return Error(APIError(f"HTTP {response.status_code}", response.status_code))
except requests.RequestException as e:
return Error(APIError(str(e), 0))
def parse_json(text: str):
"""Parse JSON and return Result[dict, APIError]."""
try:
return Ok(json.loads(text))
except json.JSONDecodeError as e:
return Error(APIError(f"Invalid JSON: {e}", 0))
def get_user_id(url: str):
"""Fetch URL, parse JSON, extract user_id."""
return (fetch_url(url)
.flat_map(parse_json)
.map(lambda data: data.get("user_id")))
get_user_id("https://api.example.com/user/1")
# Ok(123) or Error(APIError(...))This pattern shows Result's use in API operations: each step can fail with a descriptive error, and errors automatically propagate through the chain. The caller gets either the final value or the first error that occurred.
When to Use
Use Result when:
- Operations can fail with meaningful errors
- You want explicit error handling
- You need error messages/context
- You want to avoid exceptions
Don't use Result when:
- A value might be missing (use
Maybeinstead) - You need to accumulate errors (use
Validationinstead) - The operation cannot fail (use plain types)
Comparison with Maybe and Either
All three monads handle operations that might not produce a value, but they serve different purposes:
| Feature | Result | Maybe | Either |
|---|---|---|---|
| Purpose | Error handling | Optional values | Two distinct possibilities |
| Generic types | Result[T, E] (success/error) | Maybe[T] (value/none) | Either[L, R] (any two types) |
| Variants | Ok(value), Error(error) | Some(value), Nothing | Left(value), Right(value) |
| Error context | ✅ Error type only | ❌ None | ✅ Any type |
| Use case | Operations that fail | Values that might be missing | Two-value alternatives |
Examples:
# Result: Success or error with context
Result[User, str] # Ok(User) or Error("User not found")
# Maybe: Value exists or not
Maybe[User] # Some(User) or Nothing
# Either: Two possibilities with rich context
Either[APIError, User] # Left(APIError) or Right(User)Rule of thumb:
- Use Result when you have explicit success/error cases
- Use Maybe when a value might simply not exist (no error involved)
- Use Either when Left/right are two distinct possibilities (beyond success/error)
See Also
- Maybe - For optional values
- Validation - For accumulating errors
- Either - For two-value alternatives