Result Pattern
Oridecon 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 oridecon.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 oridecon.contracts.core.result and are re-exported via:
from oridecon.result import Result, Ok, Err # Canonical importfrom oridecon 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): return user.name case Err(error): return str(error)Never call unwrap() without is_ok(). unwrap() on Err raises — that is not how domain failures are supposed to travel.
Fluent Mapping
Section titled “Fluent Mapping”from oridecon.result import pipeline, Ok
# Chain operations with ResultPipeline — finalize() returns Result[T, E]name = ( pipeline(user) .then(lambda u: validate_permissions(u, required_role)) .map(lambda u: u.name) .recover(lambda _err: Ok("anonymous")) .finalize())pipeline() starts from an infallible value. .then() takes a Result-returning function; .map() is a sync transform; .recover() handles Err.
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”map / and_then are async (they take awaitables). Sync transforms use map_sync / and_then_sync.
from oridecon.result import Result, Ok, Err
async def fetch_user(user_id: str) -> Result[User, DomainError]: ...async def check_permissions(user: User) -> Result[User, PermissionError]: ...
fetched = await fetch_user("user-123")result = await fetched.and_then(check_permissions)
# Sync transformation — no extra awaitas_dict = fetched.map_sync(lambda user: user.to_dict()).map_err(lambda e: str(e))Async chaining
Section titled “Async chaining”# Each async step is awaited before the nextprofile = await fetched.map(fetch_profile)enriched = await profile.and_then(enrich_profile)
# Filter is sync — Ok → Err if the predicate failsvalid = fetched.filter(lambda u: u.is_active, InactiveUserError())4. Result Utilities
Section titled “4. Result Utilities”Decorators
Section titled “Decorators”from oridecon.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)@as_result is async-only. Sync functions use @as_result_sync. Both require at least one exception type — @as_result() with no arguments raises TypeError.
Collect and Partition
Section titled “Collect and Partition”from oridecon.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 oridecon.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 oridecon import DomainError, NotFoundErrorfrom oridecon.result import Result, Ok, Err
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 |
Coming from FastAPI: keep HTTP status mapping at the controller edge (ResultResponseMapper). Services return Result so the same method works behind a queue or a CLI — you are not throwing HTTPException out of the domain.
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.
Next Steps
Section titled “Next Steps”- Common mistakes —
unwrap()withoutis_ok() - Core Concepts — Result next to providers and DI
- Migrating from FastAPI — HTTP at the edge
- Your First App — a controller that can return
Result