Rust Essential Course – Lesson 9

Lesson 9: Error Handling

Introduction

Errors are inevitable in real programs. Rust takes a unique approach — instead of exceptions, it forces you to deal with errors explicitly at compile time. This makes Rust programs more reliable and predictable. Rust has two categories of errors: recoverable errors (using Result) and unrecoverable errors (using panic!).

Unrecoverable Errors with panic!

When something goes so wrong that your program cannot continue, Rust calls panic!. This immediately stops execution and prints an error message. You’ve already seen this happen automatically — for example, when accessing a vector out of bounds.

fn main() {
    // Explicit panic
    panic!("Something went terribly wrong!");
}

// This also panics automatically:
fn main() {
    let v = vec![1, 2, 3];
    println!("{}", v[99]); // index out of bounds -> panic!
}
  • Use panic! only for truly unrecoverable situations — bugs, impossible states.
  • For expected failure cases (bad input, missing file), use Result instead.

Recoverable Errors with Result

Result is an enum with two variants: Ok(value) for success and Err(error) for failure. Functions that can fail return a Result instead of crashing.

use std::fs;

fn main() {
    let result = fs::read_to_string("hello.txt");

    match result {
        Ok(contents) => println!("File contents: {}", contents),
        Err(e)       => println!("Error reading file: {}", e),
    }
}
  • Ok(value) — the operation succeeded; contains the result value.
  • Err(e) — the operation failed; contains the error details.
  • Use match to handle both cases explicitly.

Shortcuts: unwrap and expect

For quick prototyping when you’re confident an operation will succeed, you can use unwrap() or expect() to extract the value — but these will panic! if the result is an Err.

use std::fs;

fn main() {
    // unwrap: panics with a generic message on Err
    let contents = fs::read_to_string("hello.txt").unwrap();

    // expect: panics with YOUR message on Err (preferred over unwrap)
    let contents = fs::read_to_string("hello.txt")
        .expect("Failed to read hello.txt");

    println!("{}", contents);
}
  • Prefer expect() over unwrap() — the custom message makes debugging much easier.
  • Both should only be used when you’re certain the operation won’t fail, or in quick prototypes.

Propagating Errors with the ? Operator

The ? operator is Rust’s most ergonomic way to handle errors. If the result is Ok, it unwraps the value. If it’s Err, it returns the error early from the current function — propagating it to the caller.

use std::fs;
use std::io;

fn read_file() -> Result {
    // The ? propagates the error if reading fails
    let contents = fs::read_to_string("hello.txt")?;
    Ok(contents)
}

fn main() {
    match read_file() {
        Ok(text) => println!("Got: {}", text),
        Err(e)   => println!("Failed: {}", e),
    }
}
  • The ? operator can only be used in functions that return Result (or Option).
  • It replaces verbose match blocks and makes error-handling code much cleaner.
  • Chain multiple ? calls for multiple fallible operations in sequence.

Custom Error Messages

You can return meaningful errors from your own functions by creating a Result with a descriptive string error.

fn divide(a: f64, b: f64) -> Result {
    if b == 0.0 {
        Err(String::from("Cannot divide by zero!"))
    } else {
        Ok(a / b)
    }
}

fn main() {
    match divide(10.0, 2.0) {
        Ok(result) => println!("10 / 2 = {}", result),
        Err(e)     => println!("Error: {}", e),
    }

    match divide(5.0, 0.0) {
        Ok(result) => println!("Result: {}", result),
        Err(e)     => println!("Error: {}", e),
    }
}

Interactive Task 1: Safe Division

Write a function safe_sqrt that returns Err if the input is negative, or Ok with the square root otherwise. Call it with both a positive and a negative number and handle both cases with match.

fn safe_sqrt(n: f64) -> Result {
    // Your code here
}

fn main() {
    // Test with 25.0 and -4.0
}

Try here: https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=46b66a0c979297bd5c0c0b92b2103e9b

Interactive Task 2: The ? Operator

Write a function parse_and_double that takes a &str, parses it as an i32 using .parse(), and returns double the value. Use the ? operator to propagate any parse error. Call it with both a valid string ("21") and an invalid one ("abc").

fn parse_and_double(s: &str) -> Result {
    // Use ? to propagate the error
    // Your code here
}

fn main() {
    println!("{:?}", parse_and_double("21"));
    println!("{:?}", parse_and_double("abc"));
}

Try here: https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=c14e51c4356687bea9f895024e706c08

Mini-Challenge

Write a function first_word that takes a &str sentence and returns Ok with the first word, or Err with a message if the input is empty. Use .split_whitespace().next() to get the first word.

  • Hint: .next() returns an Option — use .ok_or() to convert it to a Result.
fn first_word(sentence: &str) -> Result {
    // Your code here
}

fn main() {
    println!("{:?}", first_word("hello world"));
    println!("{:?}", first_word(""));
}

Solution: https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=b19711b305880f16016b1ad2d3459474

Recap

  • panic! is for unrecoverable errors — bugs or impossible states that should never happen.
  • Result is for recoverable errors — use Ok(value) for success and Err(e) for failure.
  • unwrap() and expect() extract the value but panic on error — prefer expect() with a descriptive message.
  • The ? operator propagates errors up the call stack cleanly — the idiomatic Rust way to handle errors in functions.
  • Design your own fallible functions to return Result — it makes your APIs composable and explicit.

Questions about error handling? Drop a comment!

Leave a Comment

Your email address will not be published. Required fields are marked *