⚠️

Result & Error Handling β€” What Fills the begin/rescue Void

Rust handles errors as return values instead of exceptions

Ruby's error handling is exception-based. Something goes wrong, raise. Catch it somewhere with rescue. Miss it and the program dies.

# Ruby
def read_config
  File.read("config.toml")
rescue Errno::ENOENT => e
  puts "file not found: #{e.message}"
  nil
end

Rust has no exceptions. Functions that can fail return Result<T, E>.

// Rust
fn read_config() -> Result<String, std::io::Error> {
    std::fs::read_to_string("config.toml")
}

Using Result

match read_config() {
    Ok(content) => println!("{}", content),
    Err(e) => println!("file not found: {}", e),
}

Like Option, use match to branch. Ok(T) for success, Err(E) for failure.

The ? operator β€” maps to Ruby's exception propagation

In Ruby, unrescued exceptions propagate upward. In Rust, ? propagates errors to the calling function.

# Ruby β€” automatic propagation
def load_app
  config = File.read("config.toml")  # exception bubbles up
  parse(config)
end

// Rust β€” explicit propagation with ?
fn load_app() -> Result<App, Box<dyn std::error::Error>> {
    let config = std::fs::read_to_string("config.toml")?;  // returns Err here
    let app = parse(&config)?;
    Ok(app)
}

? means "if Err, return immediately; if Ok, extract the value and continue." Ruby's auto-propagation made explicit.

unwrap and expect

let config = read_config().unwrap();          // panics on Err
let config = read_config().expect("config required"); // panic with message

Common pattern: use unwrap during prototyping, replace with proper error handling later. Like Ruby's temporary rescue => e; raise e.

map_err β€” error conversion

fn load_config() -> Result<Config, AppError> {
    let content = std::fs::read_to_string("config.toml")
        .map_err(|e| AppError::Io(e))?;  // io::Error β†’ AppError
    parse_config(&content)
        .map_err(|e| AppError::Parse(e))?;
    Ok(config)
}

Like Ruby's rescue IOError => e; raise AppError, e.message pattern.

The decisive difference from Ruby

Error possibility is visible in the type. A function returning Result<T, E> declares "this function can fail" in its signature. In Ruby, you can't know which methods throw exceptions without reading docs.

Key Points

1

Result<T, E> is Ok(value) or Err(error) β€” type-safe exception replacement

2

? operator propagates errors β€” Ruby auto-propagation made explicit

3

unwrap is for prototyping β€” use match/? in production

4

Error possibility is visible in the function signature

Pros

  • Which functions can fail is obvious from types alone
  • No more production crashes from forgotten rescue

Cons

  • Defining error types is verbose β€” thiserror/anyhow crates help
  • Overusing ? can make it hard to trace where errors originated

Use Cases

File I/O, network requests β€” any operation that can fail JSON/TOML parsing β€” handling malformed input