Skip to editor content
learningrust.orglesson 11 of 26

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!

Next Steps

Now that you understand error handling, you're ready to learn about collections — the vectors, hash maps, and sets Rust uses to store and manage groups of values. Many of their operations return Option, so the handling patterns you just learned will feel right at home.

Next lesson

Collections

Learn to use Rust's standard collections including Vec, HashMap, and HashSet with iterators

25 min