Result Pattern
Lexigram discourages the use of exceptions for expected, recoverable business failures (e.g., “User Not Found” or “Validation Failed”). Instead, we use the Result Pattern, which forces explicit handling of failure states at the type level.
1. Ok and Err
Section titled “1. Ok and Err”The Result type is a union of two states: Ok (success) and Err (failure).
from lexigram.result import Result, Ok, Err
def divide(a: int, b: int) -> Result[float, str]: if b == 0: return Err("Cannot divide by zero") return Ok(a / b)Result Type Origin
Section titled “Result Type Origin”Result, Ok, and Err live in lexigram.contracts.core.result and are re-exported via:
from lexigram.result import Result, Ok, Err # Canonical importfrom lexigram import Result, Ok, Err # Also available from top-level2. Handling Results
Section titled “2. Handling Results”There are several ways to interact with a Result object.
Checking State
Section titled “Checking State”result = divide(10, 2)
if result.is_ok(): value = result.unwrap()
if result.is_err(): error = result.unwrap_err()Safe Accessors
Section titled “Safe Accessors”# Safe unwrap with defaultvalue = result.unwrap_or(0.0)
# Unwrap with callback for error casevalue = result.unwrap_or_else(lambda err: compute_fallback(err))Pattern Matching (Python 3.10+)
Section titled “Pattern Matching (Python 3.10+)”result = await service.find_user("user-42")
match result: case Ok(user): print(f"Found: {user.name}") case Err(error): print(f"Error: {error}")Fluent Mapping
Section titled “Fluent Mapping”from lexigram.result import pipeline
# Chain operations with ResultPipelinename = ( pipeline(user) .then(lambda u: validate_permissions(u, required_role)) .map(lambda u: u.name) .finalize())3. Functional Chaining
Section titled “3. Functional Chaining”You can chain operations without explicit error checks at every step. If any step returns an Err, the entire chain stops and propagates that error.
Map and Then
Section titled “Map and Then”from lexigram.result import Result, Ok, Err
async def fetch_user(user_id: str) -> Result[User, DomainError]: ...async def check_permissions(user: User) -> Result[User, PermissionError]: ...
# Chain with and_then (async)result = ( await fetch_user("user-123") .and_then(check_permissions))
# Chain with map (sync transformation)result = ( await fetch_user("user-123") .map(lambda user: user.to_dict()) .map_err(lambda e: str(e)))Async Chaining
Section titled “Async Chaining”# map — async transformation on Ok valueprofile = await result.map(fetch_profile).and_then(enrich_profile)
# Filter — convert Ok to Err if predicate failsvalid = result.filter(lambda u: u.is_active, InactiveUserError())4. Result Utilities
Section titled “4. Result Utilities”Decorators
Section titled “Decorators”from lexigram.result import as_result, as_result_sync
@as_result(IOError, TimeoutError)async def fetch(url: str) -> bytes: return await client.get(url)
@as_result_sync(ValueError, KeyError)def parse(data: str) -> int: return int(data)Collect and Partition
Section titled “Collect and Partition”from lexigram.result import collect, partition
results = [ok1, err1, ok2, err2]
# collect — short-circuits on first errorall_ok: Result[list[T], E] = collect(results)
# partition — splits into successes and failuresoks, errs = partition(results)Try/Catch
Section titled “Try/Catch”from lexigram.result import try_catch, try_catch_sync
# Asyncresult = await try_catch((IOError, TimeoutError), fetch, url)
# Syncresult = try_catch_sync((ValueError, KeyError), parse, "42")5. Error Types
Section titled “5. Error Types”Domain Errors (Expected Failures)
Section titled “Domain Errors (Expected Failures)”from lexigram.result import Resultfrom lexigram.contracts.exceptions.domain import DomainError, NotFoundError
async def find_user(self, user_id: str) -> Result[User, DomainError]: user = await self.repo.get(user_id) if not user: return Err(NotFoundError(f"User {user_id} not found")) return Ok(user)Infrastructure Errors (Propagate as Exceptions)
Section titled “Infrastructure Errors (Propagate as Exceptions)”# Database connection lost — let it propagate, don't wraptry: await self.db.connect()except DatabaseError as e: raise # Infrastructure errors propagate, not wrapped in ResultUse Result[T, E] | Use Exceptions |
|---|---|
| User not found | Database connection lost |
| Validation failed | Serialization bug |
| Payment declined | Out of memory |
| Business rule violation | Missing API key |
6. Why not Exceptions?
Section titled “6. Why not Exceptions?”- Type Safety: The return type clearly indicates that a function can fail.
- Traceability: Errors flow through the logic like data, rather than jumping “up” the stack.
- Predictability: You never have to guess which
try/exceptblock is needed. - Exhaustive Checking: With pattern matching, the compiler can verify all cases are handled.