Option and Result
Ori has no null and no exceptions. This might sound limiting, but it’s actually liberating — the type system tells you exactly what can go wrong, and the compiler ensures you handle it.
The Problem with Null
In many languages, any reference might be null:
// JavaScript
function getUser(id) {
return users.find(u => u.id === id); // might return undefined
}
let user = getUser(42);
console.log(user.name); // CRASH if user is undefined
The type system doesn’t tell you that getUser might not find anything. You have to read the documentation, study the implementation, or learn the hard way when your code crashes.
Option: Values That Might Not Exist
In Ori, when something might not exist, the type says so:
@get_user (id: int) -> Option<User> = ...;
Option<T> is a sum type with two variants:
type Option<T> = Some(T) | None;
Some(value)— a value existsNone— no value exists
Creating Options
let found = Some(42); // Has a value
let not_found: Option<int> = None; // No value
// Functions that might not return a value
@find_user (id: int) -> Option<User> = ...;
@parse_int (s: str) -> Option<int> = ...;
@first<T> (items: [T]) -> Option<T> = ...;
Pattern Matching with Option
The most direct way to handle Option:
let maybe_user = find_user(id: 42);
let message = match maybe_user {
Some(user) -> `Found: {user.name}`
None -> "User not found"
};
The compiler ensures you handle both cases. This won’t compile:
// ERROR: non-exhaustive match
let message = match maybe_user {
Some(user) -> `Found: {user.name}`
// Forgot to handle None!
};
Option Methods
is_some() and is_none() — check which variant:
let opt = Some(42);
is_some(option: opt); // true
is_none(option: opt); // false
let empty: Option<int> = None;
is_some(option: empty); // false
is_none(option: empty); // true
unwrap_or() — get value or use default:
let value = Some(42).unwrap_or(default: 0); // 42
let value = None.unwrap_or(default: 0); // 0
// Real example
let user_name = find_user(id: 42)
.map(transform: u -> u.name)
.unwrap_or(default: "Anonymous");
map() — transform the value if it exists:
let maybe_num = Some(5);
let doubled = maybe_num.map(transform: x -> x * 2); // Some(10)
let nothing: Option<int> = None;
let still_nothing = nothing.map(transform: x -> x * 2); // None
map is powerful because it lets you work with the value without manually matching:
// Without map (verbose)
let display = match find_user(id: 42) {
Some(user) -> Some(`Name: {user.name}`)
None -> None
};
// With map (concise)
let display = find_user(id: 42).map(transform: u -> `Name: {u.name}`);
and_then() — chain operations that return Options:
@find_user (id: int) -> Option<User> = ...;
@get_address (user: User) -> Option<Address> = ...;
// Chain lookups
let address = find_user(id: 42)
.and_then(transform: u -> get_address(user: u));
// Option<Address>
Without and_then, you’d need nested matches:
let address = match find_user(id: 42) {
None -> None
Some(user) -> match get_address(user: user) {
None -> None
Some(addr) -> Some(addr)
}
};
// Much more verbose!
filter() — keep value only if it matches a condition:
let positive = Some(5).filter(predicate: x -> x > 0); // Some(5)
let filtered = Some(-3).filter(predicate: x -> x > 0); // None
let nothing = None.filter(predicate: x -> x > 0); // None
The Coalesce Operator ??
Use ?? as shorthand for “value or default”:
let name = maybe_name ?? "Anonymous";
let count = parse_int(s: input) ?? 0;
let config = load_config() ?? default_config;
This is equivalent to unwrap_or:
maybe_name ?? "Anonymous";
maybe_name.unwrap_or(default: "Anonymous");
// Same result
When to Use Option
Use Option<T> when:
- A value legitimately might not exist
- Absence is a normal, expected case
- The caller should decide what to do when there’s no value
Examples:
- Looking up a user by ID (might not exist)
- Getting the first element of a list (might be empty)
- Parsing a string to a number (might be invalid)
- Finding an element that matches a condition (might not find one)
Result: Operations That Can Fail
While Option represents “might not exist,” Result represents “might fail.”
type Result<T, E> = Ok(T) | Err(E);
Ok(value)— operation succeededErr(error)— operation failed
Creating Results
let success: Result<int, str> = Ok(42);
let failure: Result<int, str> = Err("something went wrong");
// Functions that can fail
@read_file (path: str) -> Result<str, Error> uses FileSystem = ...;
@parse_json (data: str) -> Result<Config, ParseError> = ...;
@connect (url: str) -> Result<Connection, NetworkError> uses Http = ...;
Pattern Matching with Result
let result = read_file(path: "config.json");
match result {
Ok(content) -> process(data: content)
Err(e) -> print(msg: `Error: {e.message}`)
};
Result Methods
is_ok() and is_err() — check which variant:
let result = Ok(42);
is_ok(result: result); // true
is_err(result: result); // false
unwrap_or() — get value or use default:
let value = Ok(42).unwrap_or(default: 0); // 42
let value = Err("oops").unwrap_or(default: 0); // 0
map() — transform the success value:
let result = Ok(5);
let doubled = result.map(transform: x -> x * 2); // Ok(10)
let error: Result<int, str> = Err("failed");
let still_error = error.map(transform: x -> x * 2); // Err("failed")
map_err() — transform the error:
let result: Result<int, str> = Err("raw error");
let wrapped = result.map_err(transform: e -> Error { message: e, code: 500 });
// Err(Error { message: "raw error", code: 500 })
and_then() — chain fallible operations:
@read_file (path: str) -> Result<str, Error> = ...;
@parse_config (data: str) -> Result<Config, Error> = ...;
let config = read_file(path: "config.json")
.and_then(transform: data -> parse_config(data: data));
// Result<Config, Error>
ok() — convert to Option (discards error):
let result: Result<int, str> = Ok(42);
result.ok(); // Some(42)
let error: Result<int, str> = Err("failed");
error.ok(); // None
err() — get the error as Option:
let error: Result<int, str> = Err("failed");
error.err(); // Some("failed")
let success: Result<int, str> = Ok(42);
success.err(); // None
The ?? Operator with Result
You can use ?? with Result too:
let value = risky_operation() ?? default_value;
This extracts the Ok value or uses the default if it’s an Err.
Converting Between Option and Result
Option to Result — provide an error for the None case:
let maybe_user = find_user(id: 42);
let result = maybe_user.ok_or(error: Error { message: "User not found" });
// Result<User, Error>
Result to Option — discard the error:
let result = risky_operation();
let maybe = result.ok(); // Option<T>, error is lost
Assertions
Ori provides assertions for testing Option and Result:
// Option assertions
assert_some(option: find_user(id: 1));
assert_none(option: find_user(id: -1));
// Result assertions
assert_ok(result: parse_int(text: "42"));
assert_err(result: parse_int(text: "not a number"));
Common Patterns
Safe Indexing
@safe_get<T> (items: [T], index: int) -> Option<T> =
if index < 0 || index >= len(collection: items) then
None
else
Some(items[index]);
Default on Error
let config = load_config().unwrap_or(default: Config.default());
let user = fetch_user(id: id).unwrap_or(default: guest_user);
First Success
@try_sources (id: int) -> Option<Data> = {
// Try cache first
let cached = cache_lookup(id: id);
if is_some(option: cached) then return cached;
// Try database
let from_db = db_lookup(id: id);
if is_some(option: from_db) then return from_db;
// Try remote
remote_lookup(id: id).ok()
}
Chaining Multiple Optionals
@get_user_city (id: int) -> Option<str> =
find_user(id: id)
.and_then(transform: u -> u.address)
.and_then(transform: a -> a.city);
Complete Example
type User = { id: int, name: str, email: str }
type ValidationError = { field: str, message: str }
// Parse ID from string
@parse_id (s: str) -> Option<int> =
s as? int;
@test_parse_id tests @parse_id () -> void = {
assert_eq(actual: parse_id(s: "42"), expected: Some(42));
assert_eq(actual: parse_id(s: "abc"), expected: None)
}
// Validate email format
@validate_email (email: str) -> Option<str> =
if email.contains(substring: "@") then Some(email) else None;
@test_validate_email tests @validate_email () -> void = {
assert_some(option: validate_email(email: "test@example.com"));
assert_none(option: validate_email(email: "invalid"))
}
// Validate user data
@validate_user (name: str, email: str) -> Result<void, [ValidationError]> = {
let errors: [ValidationError] = [];
let errors = if is_empty(collection: name) then
[...errors, ValidationError { field: "name", message: "Name required" }]
else errors;
let errors = if is_none(option: validate_email(email: email)) then
[...errors, ValidationError { field: "email", message: "Invalid email" }]
else errors;
if is_empty(collection: errors) then Ok(()) else Err(errors)
}
@test_validate_user tests @validate_user () -> void = {
assert_ok(result: validate_user(name: "Alice", email: "a@b.com"));
assert_err(result: validate_user(name: "", email: "invalid"))
}
// Process user request
@process_request (id_str: str) -> str = {
let id = parse_id(s: id_str);
match id {
None -> "Invalid ID format"
Some(id) -> match find_user(id: id) {
None -> `User {id} not found`
Some(user) -> `Found: {user.name}`
}
}
}
// Simulated user lookup
@find_user (id: int) -> Option<User> = match id {
1 -> Some(User { id: 1, name: "Alice", email: "alice@example.com" })
2 -> Some(User { id: 2, name: "Bob", email: "bob@example.com" })
_ -> None
};
@test_find_user tests @find_user () -> void = {
assert_some(option: find_user(id: 1));
assert_none(option: find_user(id: 999))
}
@test_process_request tests @process_request () -> void = {
assert_eq(actual: process_request(id_str: "abc"), expected: "Invalid ID format");
assert_eq(actual: process_request(id_str: "1"), expected: "Found: Alice");
assert_eq(actual: process_request(id_str: "999"), expected: "User 999 not found")
}
Quick Reference
Option
type Option<T> = Some(T) | None;
// Create
let some = Some(42);
let none: Option<int> = None;
// Check
is_some(option: opt); is_none(option: opt);
// Extract
opt.unwrap_or(default: val);
opt ?? default;
// Transform
opt.map(transform: fn);
opt.and_then(transform: fn);
opt.filter(predicate: fn);
// Convert
opt.ok_or(error: err); // -> Result
Result
type Result<T, E> = Ok(T) | Err(E);
// Create
let ok = Ok(42);
let err = Err("failed");
// Check
is_ok(result: res); is_err(result: res);
// Extract
res.unwrap_or(default: val);
res ?? default;
// Transform
res.map(transform: fn);
res.map_err(transform: fn);
res.and_then(transform: fn);
// Convert
res.ok(); // -> Option<T>
res.err(); // -> Option<E>
What’s Next
Now that you understand Option and Result:
- Error Propagation — The
?operator and error traces - Panic and Recovery — Unrecoverable errors and contracts