Skip to main content

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

MethodReturnsDescription
.is_ok()boolReturns true if the result is an ok variant
.is_err()boolReturns true if the result is an err variant
.to_string()stringString 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

MethodReturnsDescription
.is_some()boolReturns true if the optional contains a value
.is_none()boolReturns true if the optional is empty
.to_string()stringString 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 return statements
  • 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

  1. Return result for fallible operations - Parse failures, I/O operations, validation
  2. Return optional for nullable values - Collection access, lookups, searches
  3. Match exhaustively - Handle both success and error cases
  4. Use descriptive error messages - Include context in error strings
  5. Early returns for errors - Reduces nesting
  6. Use _ for ignored values - Makes intent explicit
  7. Don't overuse wildcards - Match specific cases when possible
  8. Document error conditions - What errors can a function return?
  9. Chain operations explicitly - No ? operator, use match
  10. 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