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:
interface Error {
func message() returns string
}
string implements Error, so existing result<T, string> code continues to work.
Basic Usage
func divide(int a, int b) returns result<int, string> {
if b == 0 {
return err("division by zero")
}
return ok(a / b)
}
auto result = divide(10, 2)
match result {
ok(value) {
print("result: " + value.to_string()) // "result: 5"
}
err(error) {
print("Error: " + error)
}
}
result Variants
enum result<T, E> {
ok(T) // Success case with value
err(E) // Error case with error value
}
Creating result Values
// Success
auto success = ok(42) // result<int, E>
auto success2 = ok("completed") // result<string, E>
// Failure
auto failure = err("something went wrong") // result<T, string>
// Explicit typing when needed
result<int, string> result = ok(100)
result Methods
| Method | Returns | Description |
|---|---|---|
.is_ok() | bool | Returns true if the result is an ok variant |
.is_err() | bool | Returns true if the result is an err variant |
.to_string() | string | String representation |
result<int, string> res1 = ok(42)
result<int, string> res2 = err("error")
print(res1.is_ok().to_string()) // true
print(res1.is_err().to_string()) // false
print(res2.is_ok().to_string()) // false
print(res2.is_err().to_string()) // true
Pattern Matching Results
func parse_int(string s) returns result<int, string> {
auto result = s.to_int() // returns result<int, string>
return result
}
auto parsed = parse_int("42")
match parsed {
ok(value) {
auto message = "Parsed: " + value.to_string()
print(message)
}
err(error) {
print("Parse error: " + error)
}
}
Ignoring Error Details
Use _ when you don't need the error value:
match result {
ok(value) {
print("Success: " + value.to_string())
}
err(_) {
print("some error occurred") // don't care about details
}
}
optional<T>
The optional type represents values that may or may not exist.
Basic Usage
func findEven(list<int> xs) returns optional<int> {
for x in xs {
if x % 2 == 0 {
return some(x)
}
}
return none
}
auto maybeEven = findEven([1, 3, 4, 7])
match maybeEven {
some(value) {
print("Found even: " + value.to_string()) // "Found even: 4"
}
none {
print("No even number found")
}
}
optional Methods
| Method | Returns | Description |
|---|---|---|
.is_some() | bool | Returns true if the optional contains a value |
.is_none() | bool | Returns true if the optional is empty |
.to_string() | string | String representation |
optional<int> opt1 = some(42)
optional<int> opt2 = none
print(opt1.is_some().to_string()) // true
print(opt1.is_none().to_string()) // false
print(opt2.is_some().to_string()) // false
print(opt2.is_none().to_string()) // true
result Methods
enum optional<T> {
some(T) // Value present
none // Value absent
}
Creating optional Values
// With value
auto present = some(42) // optional<int>
auto present2 = some("hello") // optional<string>
// Without value
auto absent = none // optional<T> (generic)
// Explicit typing
optional<int> maybeNumber = some(100)
optional<string> maybeText = none
Safe Collection Access
Collections return optional<T> for safe access:
auto nums = [10, 20, 30]
// Safe access with .get()
match nums.get(0) {
some(first) {
print("First: " + first.to_string()) // "First: 10"
}
none {
print("Index out of bounds")
}
}
// Out of bounds
match nums.get(100) {
some(value) {
print("Found: " + value.to_string())
}
none {
print("Index out of bounds") // This prints
}
}
Map Lookups
auto scores = {"Alice": 90, "Bob": 85}
match scores.get("Alice") {
some(score) {
print("Alice's score: " + score.to_string())
}
none {
print("Student not found")
}
}
match scores.get("Charlie") {
some(score) {
print("Score: " + score.to_string())
}
none {
print("Charlie not found") // This prints
}
}
Ignoring the Value
Use _ when you only care about presence/absence:
match maybeValue {
some(_) {
print("Got a value") // don't care what it is
}
none {
print("Got nothing")
}
}
Combining result and optional
optional of result
func tryParse(optional<string> maybeStr) returns optional<result<int, string>> {
match maybeStr {
some(s) {
return some(s.to_int()) // result<int, string>
}
none {
return none
}
}
}
result of optional
func getRequired(map<string, int> data, string key) returns result<int, string> {
match data.get(key) {
some(value) {
return ok(value)
}
none {
return err("Key '" + key + "' not found")
}
}
}
Error Propagation Patterns
Early Returns
func processData(string input) returns result<int, string> {
// Validate input
if input == "" {
return err("empty input")
}
// Parse input
auto parsed = input.to_int()
match parsed {
ok(value) {
// Continue processing
if value < 0 {
return err("negative values not allowed")
}
return ok(value * 2)
}
err(msg) {
return err("parse error: " + msg)
}
}
}
Nested Matching
func complexOperation() returns result<string, string> {
auto step1 = firstOperation()
match step1 {
ok(value1) {
auto step2 = secondOperation(value1)
match step2 {
ok(value2) {
return ok(value2)
}
err(err2) {
return err("step2 failed: " + err2)
}
}
}
err(err1) {
return err("step1 failed: " + err1)
}
}
}
Fallible Type Conversions
String and char parsing return result because they can fail:
// String to int
auto num_str = "42"
auto result = num_str.to_int() // result<int, string>
match result {
ok(value) {
print("Parsed: " + value.to_string())
}
err(error) {
print("Parse error: " + error)
}
}
// String to float
auto float_str = "3.14159"
auto float_result = float_str.to_float() // result<float, string>
// Char to digit (only works for '0'-'9')
auto digit_char = '5'
auto digit_result = digit_char.to_int() // result<int, string>
match digit_result {
ok(digit) { print(digit.to_string()) } // "5"
err(msg) { print(msg) }
}
// Non-digit character
auto letter = 'A'
auto letter_result = letter.to_int()
match letter_result {
ok(_) { print("Unexpected success") }
err(msg) { print(msg) } // "Character is not a digit (0-9)"
}
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: Simple with match statements
- 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
?operator for error propagation (Mux doesn't)
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
- Early returns for errors - Reduces nesting
- 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?
- Chain operations explicitly - No
?operator, use match - Prefer result over panicking - Explicit > implicit
Common Patterns
Validation
func validateAge(int age) returns result<int, string> {
if age < 0 {
return err("age cannot be negative")
}
if age > 150 {
return err("age too large")
}
return ok(age)
}
Lookup with Default
func getOrDefault(map<string, int> data, string key, int default) returns int {
match data.get(key) {
some(value) { return value }
none { return default }
}
}
Transform result
func doubleIfEven(int n) returns result<int, string> {
if n % 2 == 0 {
return ok(n * 2)
}
return err("number is not even")
}
func processNumber(int n) returns result<string, string> {
match doubleIfEven(n) {
ok(doubled) {
return ok("Doubled: " + doubled.to_string())
}
err(msg) {
return err(msg)
}
}
}
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