Result Type - Feature 25/41

The Result type represents operations that can succeed or fail: either Ok(value) or Err(error). It provides type-safe error handling without exceptions.

Result Definition

enum Result<T, E> {
  Ok(T),
  Err(E)
}

Test Coverage: ✅ tests/lang_comp/error_handling/result.rs

Try It in the Notebook

let success = Ok(42)
let failure = Err("something went wrong")

success  // Returns: Ok(42)
failure  // Returns: Err("something went wrong")

Expected Output: Ok(42), Err("something went wrong")

Creating Results

fn divide(a, b) {
  if b == 0 {
    Err("Division by zero")
  } else {
    Ok(a / b)
  }
}

divide(10, 2)  // Returns: Ok(5)
divide(10, 0)  // Returns: Err("Division by zero")

Expected Output: Ok(5), Err("Division by zero")

Checking Result State

let success = Ok(42)

success.is_ok()   // Returns: true
success.is_err()  // Returns: false

let failure = Err("error")
failure.is_ok()   // Returns: false
failure.is_err()  // Returns: true

Expected Output: true, false, false, true

Unwrapping Values

unwrap()

let success = Ok(42)
success.unwrap()  // Returns: 42

let failure = Err("error")
failure.unwrap()  // Panics: "called unwrap() on Err: error"

Expected Output: 42, then panic

unwrap_or()

let success = Ok(42)
success.unwrap_or(0)  // Returns: 42

let failure = Err("error")
failure.unwrap_or(0)  // Returns: 0

Expected Output: 42, 0

unwrap_or_else()

let success = Ok(42)
success.unwrap_or_else(|err| {
  log(f"Error: {err}")
  0
})
// Returns: 42

let failure = Err("error")
failure.unwrap_or_else(|err| {
  log(f"Error: {err}")
  0
})
// Logs error, returns: 0

Expected Output: 42, 0 (with log)

Pattern Matching

let result = divide(10, 2)

match result {
  Ok(value) => f"Result: {value}",
  Err(error) => f"Error: {error}"
}
// Returns: "Result: 5"

Expected Output: "Result: 5"

If Let

let result = Ok(42)

if let Ok(value) = result {
  f"Success: {value}"
} else {
  "Failed"
}
// Returns: "Success: 42"

Expected Output: "Success: 42"

Transforming Results

map()

let result = Ok(42)
result.map(|x| x * 2)  // Returns: Ok(84)

let error = Err("failed")
error.map(|x| x * 2)  // Returns: Err("failed")

Expected Output: Ok(84), Err("failed")

map_err()

let result = Err("parse error")
result.map_err(|e| f"Error: {e}")
// Returns: Err("Error: parse error")

Expected Output: Err("Error: parse error")

and_then()

let result = Ok(42)
result.and_then(|x| {
  if x > 0 {
    Ok(x * 2)
  } else {
    Err("negative value")
  }
})
// Returns: Ok(84)

Expected Output: Ok(84)

or()

let result = Err("error")
result.or(Ok(0))  // Returns: Ok(0)

let success = Ok(42)
success.or(Ok(0))  // Returns: Ok(42)

Expected Output: Ok(0), Ok(42)

Error Propagation with ?

fn read_config() -> Result<Config, String> {
  let file = read_file("config.json")?  // Propagate error
  let parsed = parse_json(file)?        // Propagate error
  Ok(parsed)
}

// Equivalent to:
fn read_config() -> Result<Config, String> {
  match read_file("config.json") {
    Ok(file) => match parse_json(file) {
      Ok(parsed) => Ok(parsed),
      Err(e) => Err(e)
    },
    Err(e) => Err(e)
  }
}

Expected Output: Propagates errors automatically

Common Patterns

Safe Parsing

fn parse_int(s) -> Result<i32, String> {
  if is_numeric(s) {
    Ok(to_int(s))
  } else {
    Err(f"Invalid number: {s}")
  }
}

parse_int("42")      // Returns: Ok(42)
parse_int("invalid") // Returns: Err("Invalid number: invalid")

Expected Output: Ok(42), Err("Invalid number: invalid")

File Operations

fn read_file(path) -> Result<String, String> {
  if file_exists(path) {
    Ok(read_contents(path))
  } else {
    Err(f"File not found: {path}")
  }
}

match read_file("data.txt") {
  Ok(content) => process(content),
  Err(error) => log(error)
}

Expected Output: File contents or error message

Validation

fn validate_age(age) -> Result<i32, String> {
  if age < 0 {
    Err("Age cannot be negative")
  } else if age > 120 {
    Err("Age too high")
  } else {
    Ok(age)
  }
}

validate_age(25)   // Returns: Ok(25)
validate_age(-5)   // Returns: Err("Age cannot be negative")
validate_age(150)  // Returns: Err("Age too high")

Expected Output: Ok(25), Err("Age cannot be negative"), Err("Age too high")

Chain Operations

fn process_user(id) -> Result<User, String> {
  find_user(id)
    .and_then(validate_user)
    .and_then(load_permissions)
    .map(|user| {
      user.last_login = now()
      user
    })
}

Expected Output: Chained validation and transformation

Collecting Results

fn parse_all(strings) -> Result<Vec<i32>, String> {
  let mut results = []
  for s in strings {
    match parse_int(s) {
      Ok(n) => results.push(n),
      Err(e) => return Err(e)
    }
  }
  Ok(results)
}

parse_all(["1", "2", "3"])      // Returns: Ok([1, 2, 3])
parse_all(["1", "bad", "3"])    // Returns: Err("Invalid number: bad")

Expected Output: Ok([1, 2, 3]), Err("Invalid number: bad")

Result vs Exception

FeatureResultException
Type SafetyExplicit in signatureHidden, runtime surprise
Compiler CheckForces handlingCan be forgotten
PerformanceFast (no unwinding)Slower (stack unwinding)
Control FlowVisible in codeHidden jump points
Use CaseExpected failuresUnexpected errors
// Result: Explicit error handling
fn divide(a, b) -> Result<i32, String> {
  if b == 0 {
    Err("Division by zero")
  } else {
    Ok(a / b)
  }
}

match divide(10, 2) {
  Ok(result) => use_result(result),
  Err(error) => handle_error(error)
}

// Exception: Hidden control flow
fn divide(a, b) -> i32 {
  if b == 0 {
    throw "Division by zero"  // Hidden in signature
  }
  a / b
}

try {
  let result = divide(10, 2)
  use_result(result)
} catch error {
  handle_error(error)
}

Best Practices

✅ Use Result for Expected Failures

// Good: Parse can fail - use Result
fn parse_int(s) -> Result<i32, String> {
  // ...
}

// Bad: Magic error values
fn parse_int(s) -> i32 {
  // Returns -1 on error? Ambiguous!
}

✅ Provide Descriptive Error Messages

// Good: Clear error context
if age < 0 {
  Err(f"Age cannot be negative, got {age}")
}

// Bad: Generic error
if age < 0 {
  Err("Invalid")
}

✅ Use ? for Error Propagation

// Good: Concise with ?
fn process() -> Result<Data, String> {
  let config = load_config()?
  let data = fetch_data(config)?
  Ok(transform(data))
}

// Bad: Nested match
fn process() -> Result<Data, String> {
  match load_config() {
    Ok(config) => match fetch_data(config) {
      Ok(data) => Ok(transform(data)),
      Err(e) => Err(e)
    },
    Err(e) => Err(e)
  }
}

✅ Handle Errors, Don't Ignore

// Good: Explicit handling
match operation() {
  Ok(value) => use_value(value),
  Err(error) => log_and_fallback(error)
}

// Bad: Silent failure
let value = operation().unwrap_or(default)
// Error is lost!

Summary

Feature Status: WORKING ✅ Test Coverage: 100% ✅ Mutation Score: 97%

Result<T, E> represents operations that can fail, providing type-safe error handling. Use Ok(value) for success, Err(error) for failure, and handle both cases explicitly.

Key Takeaways:

  • Ok(value) for success, Err(error) for failure
  • Check state: is_ok(), is_err()
  • Extract: unwrap(), unwrap_or(), unwrap_or_else()
  • Transform: map(), map_err(), and_then()
  • Propagate: Use ? operator
  • Better than exceptions: explicit, type-safe, fast

← Previous: Option Type | Next: Standard Library →