Error Handling
Mux uses explicit error handling through the result<T, E> and optional<T> types, avoiding exceptions entirely.
result<T, E>
The result type represents operations that can succeed with a value of type T or fail with an error of type E.
E must implement the Error interface:
string implements Error, so it can be used as a Result's error type.
Propagating with use
use extracts a successful value. If the operation fails, it returns the error
from the enclosing function immediately. Sequential operations stay flat:
For an Optional, use extracts some(value) or returns none:
The enclosing function must return the same wrapper kind. Result error types
must be compatible; the success types may differ. An Optional does not need an
error value. There is no automatic conversion between none and err.
use is a prefix expression with unary precedence. Calls and member access bind
more tightly, so use text.to_int() extracts the call's result. Write
(use text.to_int()).to_string() to call a method on the extracted integer.
It also works in arguments, collection elements, assignments, conditions, and
return expressions. A standalone use operation() handles a Result with a
void success payload. Each operand runs once, and ordinary short-circuit rules
still apply. Inside a lambda, failure returns from that lambda.
Functions returning result<void, E> use ok() for a successful completion;
value-carrying Results continue to use ok(value).
Checking before extracting
Use inspection methods when failure needs local handling:
The compiler checks that value() or error() is valid on every path reaching
the access. Printing an error and continuing does not make value() safe.
Optional values follow the same rule with is_some(), is_none(), and
value(). Checks can establish facts through branches and saved booleans, but
changing the checked value invalidates those facts. Access is rejected when
the compiler cannot prove the variant.
Basic Usage
Assigning from a match
match can produce a value. Keep each arm readable across several lines: a
successful arm yields the value, while a failure arm can explicitly return the
current function's error.
result Variants
result<T, E> is built in, not a user enum. It has two cases - ok carrying a
T, and err carrying an E - and you construct them with the ok and err
functions rather than through an enum name. Declaring your own type named
result is an error, because it used to overwrite the built-in silently.
The error type must implement Error, which means supplying message().
string does, so result<int, string> works out of the box; a class can too:
Creating result Values
result Methods
| Method | Returns | Description |
|---|---|---|
.is_ok() | bool | Tests for success; narrows checked branches |
.is_err() | bool | Tests for failure; narrows checked branches |
.value() | T | Extracts the payload when success is proved |
.error() | E | Extracts the payload when failure is proved |
.to_string() | string | String representation |
Use match for pattern-specific behavior and transformations of both outcomes.
Pattern Matching Results
Ignoring Error Details
Use _ when you don't need the error value:
optional<T>
The optional type represents values that may or may not exist.
Basic Usage
optional Methods
| Method | Returns | Description |
|---|---|---|
.is_some() | bool | Tests for a present value; narrows checked branches |
.is_none() | bool | Tests for absence; narrows checked branches |
.value() | T | Extracts the payload when presence is proved |
.to_string() | string | String representation |
Use use to propagate absence, inspection for local handling, and match
when both cases need different behavior.
Optional variants
optional<T> is built in and has two cases - some carrying a T, and
none. Like result, you build it with the some and none functions rather
than through an enum name, and you cannot declare a type named optional
yourself:
Creating optional Values
Safe Collection Access
Collections return optional<T> for safe access:
Map Lookups
Ignoring the Value
Use _ when you only care about presence/absence:
Combining result and optional
optional of result
result of optional
Error Propagation Patterns
Early Returns
Flat Sequential Propagation
When each step should propagate its error unchanged, use keeps the happy
path flat and makes the enclosing return type explicit. Use a match when an
error needs local transformation or both outcomes require distinct work.
Fallible Type Conversions
String and char parsing return result because they can fail:
Technical Implementation
Memory Layout
Both types use a uniform runtime representation:
pub struct result<T, E> {
discriminant: i32, // 0 = ok, 1 = err
data: *mut T, // pointer to value
}
pub struct optional<T> {
discriminant: i32, // 0 = none, 1 = some
data: *mut T, // pointer to value
}Benefits:
- Single runtime representation: Collections can store either
- No enum overhead: No runtime enum tag beyond discriminant
- Easy error propagation:
usekeeps sequential operations flat - Interop: optional and result can wrap the same types
Runtime ABI note
• Implementation detail: the runtime now represents both optional<T> and result<T, E> as boxed Value pointers (*mut Value) at the FFI boundary. This means runtime constructors and C-exported helpers return *mut Value for these types. Compiler-generated code and native extensions should treat optionals/results as boxed Value objects and use the provided discriminant helpers when matching variants.
Comparison with Other Languages
vs Rust
Very similar:
// Rust
fn divide(a: i32, b: i32) -> result<i32, String> {
if b == 0 {
err("division by zero".to_string())
} else {
ok(a / b)
}
}
// Mux
func divide(int a, int b) returns result<int, string> {
if b == 0 {
return err("division by zero")
}
return ok(a / b)
}Differences:
- Mux uses explicit
returnstatements - Rust has the
?operator; Mux uses the explicituseprefix instead
vs Go
Similar philosophy, different syntax:
// Go
func divide(a, b int) (int, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}
// Mux
func divide(int a, int b) returns result<int, string> {
if b == 0 {
return err("division by zero")
}
return ok(a / b)
}Mux advantage:
- Type system enforces error handling
- Cannot ignore errors without explicit match
vs Exceptions (Java/Python)
Different paradigm:
# Python
def divide(a, b):
if b == 0:
raise ValueError("division by zero")
return a / b
# Mux
func divide(int a, int b) returns result<int, string> {
if b == 0 {
return err("division by zero")
}
return ok(a / b)
}Mux advantages:
- Errors visible in function signature
- Cannot forget to handle errors
- No runtime exceptions
- No try/catch blocks
Best Practices
- Return result for fallible operations - Parse failures, I/O operations, validation
- Return optional for nullable values - Collection access, lookups, searches
- Match exhaustively - Handle both success and error cases
- Use descriptive error messages - Include context in error strings
- Use
usefor propagation - Reduces nesting while preserving typed errors - Use
_for ignored values - Makes intent explicit - Don't overuse wildcards - Match specific cases when possible
- Document error conditions - What errors can a function return?
- Flatten sequential fallible operations - Use
useto propagate failure, or inspection methods and early returns for custom handling - Prefer result over panicking - Explicit > implicit
Common Patterns
Validation
Lookup with Default
Transform result
See Also
- Enums - result and optional as tagged unions
- Control Flow - Pattern matching with match
- Types - Fallible type conversions
- Collections - Safe collection access with optional