Error Handling in Rust

Rust groups errors into two categories: recoverable errors (like file not found) and unrecoverable errors (like accessing an array out of bounds). Instead of exceptions, Rust uses Result<T, E> for recoverable errors and panic! for unrecoverable ones.

Unrecoverable Errors with panic!

When something goes terribly wrong and there's no way to recover:

fn main() {
    // This will panic and stop the program
    panic!("crash and burn!");
}

Common situations that cause panics:

fn main() {
    let v = vec![1, 2, 3];

    // This will panic: index out of bounds
    // v[99];  // Uncommenting this will crash

    println!("This won't print if we panic above");
}

The Result Enum

For recoverable errors, Rust uses the Result type:

enum Result<T, E> {
    Ok(T),   // Success, contains the value
    Err(E),  // Error, contains error info
}

Basic Usage

use std::fs::File;

fn main() {
    let file_result = File::open("hello.txt");

    let file = match file_result {
        Ok(f) => f,
        Err(e) => {
            println!("Failed to open file: {}", e);
            return;
        }
    };

    println!("File opened successfully!");
}

Handling Different Error Types

use std::fs::File;
use std::io::ErrorKind;

fn main() {
    let file = File::open("hello.txt");

    let file = match file {
        Ok(f) => f,
        Err(error) => match error.kind() {
            ErrorKind::NotFound => {
                println!("File not found, creating it...");
                match File::create("hello.txt") {
                    Ok(fc) => fc,
                    Err(e) => panic!("Couldn't create file: {:?}", e),
                }
            }
            other_error => {
                panic!("Problem opening file: {:?}", other_error);
            }
        },
    };
}

Shortcuts: unwrap and expect

For prototyping or when you're sure an operation will succeed:

use std::fs::File;

fn main() {
    // unwrap: panics with generic message if Err
    // let f = File::open("hello.txt").unwrap();

    // expect: panics with your custom message if Err
    let f = File::open("hello.txt")
        .expect("Failed to open hello.txt");
}

Propagating Errors

Often you want to return errors to the caller:

use std::fs::File;
use std::io::{self, Read};

fn read_username_from_file() -> Result<String, io::Error> {
    let file = File::open("username.txt");

    let mut file = match file {
        Ok(f) => f,
        Err(e) => return Err(e),
    };

    let mut username = String::new();

    match file.read_to_string(&mut username) {
        Ok(_) => Ok(username),
        Err(e) => Err(e),
    }
}

fn main() {
    match read_username_from_file() {
        Ok(name) => println!("Username: {}", name),
        Err(e) => println!("Error reading username: {}", e),
    }
}

The ? Operator

The ? operator provides a concise way to propagate errors:

use std::fs::File;
use std::io::{self, Read};

fn read_username_from_file() -> Result<String, io::Error> {
    let mut file = File::open("username.txt")?;
    let mut username = String::new();
    file.read_to_string(&mut username)?;
    Ok(username)
}

// Even more concise with chaining:
fn read_username_short() -> Result<String, io::Error> {
    let mut username = String::new();
    File::open("username.txt")?.read_to_string(&mut username)?;
    Ok(username)
}

fn main() {
    match read_username_from_file() {
        Ok(name) => println!("Username: {}", name),
        Err(e) => println!("Error: {}", e),
    }
}

Using ? with Option

The ? operator also works with Option:

fn last_char_of_first_line(text: &str) -> Option<char> {
    text.lines().next()?.chars().last()
}

fn main() {
    let text = "Hello\nWorld";
    match last_char_of_first_line(text) {
        Some(c) => println!("Last char: {}", c),
        None => println!("No character found"),
    }
}

Creating Custom Errors

You can define your own error types:

use std::fmt;

#[derive(Debug)]
struct ValidationError {
    message: String,
}

impl fmt::Display for ValidationError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "Validation error: {}", self.message)
    }
}

fn validate_age(age: i32) -> Result<(), ValidationError> {
    if age < 0 {
        Err(ValidationError {
            message: String::from("Age cannot be negative"),
        })
    } else if age > 150 {
        Err(ValidationError {
            message: String::from("Age seems unrealistic"),
        })
    } else {
        Ok(())
    }
}

fn main() {
    match validate_age(25) {
        Ok(()) => println!("Age is valid"),
        Err(e) => println!("{}", e),
    }

    match validate_age(-5) {
        Ok(()) => println!("Age is valid"),
        Err(e) => println!("{}", e),
    }
}

When to Use panic! vs Result

Use panic! when:

  • The error is unrecoverable (bug in code, corrupted data)
  • In examples, prototypes, or tests
  • When you're absolutely sure the code won't fail

Use Result when:

  • The error is expected and recoverable
  • The caller should decide how to handle it
  • In library code (let users decide)

Practice Exercise

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

fn main() {
    // Try different values
    let calculations = vec![
        (10.0, 2.0),
        (10.0, 0.0),
        (100.0, 4.0),
    ];

    for (a, b) in calculations {
        match divide(a, b) {
            Ok(result) => println!("{} / {} = {}", a, b, result),
            Err(e) => println!("{} / {} failed: {}", a, b, e),
        }
    }
}

Key Takeaways

  • Rust has no exceptions - use Result and panic!
  • panic! for unrecoverable errors that crash the program
  • Result<T, E> for recoverable errors with Ok(T) or Err(E)
  • Use unwrap() and expect() for quick prototyping
  • The ? operator propagates errors concisely
  • Prefer Result in library code to give callers control
  • Create custom error types for domain-specific errors

Proper error handling makes your Rust programs robust and reliable!

Error Handling | LearningRust.org