Skip to content
Packages Examples Agents Blog Get started

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.

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, Ok, and Err live in oridecon.contracts.core.result and are re-exported via:

from oridecon.result import Result, Ok, Err # Canonical import
from oridecon import Result, Ok, Err # Also available from top-level

There are several ways to interact with a Result object.

result = divide(10, 2)
if result.is_ok():
value = result.unwrap()
if result.is_err():
error = result.unwrap_err()
# Safe unwrap with default
value = result.unwrap_or(0.0)
# Unwrap with callback for error case
value = result.unwrap_or_else(lambda err: compute_fallback(err))
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.

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.


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 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 await
as_dict = fetched.map_sync(lambda user: user.to_dict()).map_err(lambda e: str(e))
# Each async step is awaited before the next
profile = await fetched.map(fetch_profile)
enriched = await profile.and_then(enrich_profile)
# Filter is sync — Ok → Err if the predicate fails
valid = fetched.filter(lambda u: u.is_active, InactiveUserError())

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.

from oridecon.result import collect, partition
results = [ok1, err1, ok2, err2]
# collect — short-circuits on first error
all_ok: Result[list[T], E] = collect(results)
# partition — splits into successes and failures
oks, errs = partition(results)
from oridecon.result import try_catch, try_catch_sync
# Async
result = await try_catch((IOError, TimeoutError), fetch, url)
# Sync
result = try_catch_sync((ValueError, KeyError), parse, "42")

from oridecon import DomainError, NotFoundError
from 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 wrap
try:
await self.db.connect()
except DatabaseError as e:
raise # Infrastructure errors propagate, not wrapped in Result
Use Result[T, E]Use Exceptions
User not foundDatabase connection lost
Validation failedSerialization bug
Payment declinedOut of memory
Business rule violationMissing 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.


  • 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/except block is needed.
  • Exhaustive Checking: With pattern matching, the compiler can verify all cases are handled.