Error handling in Rust

About This Module

This module covers error handling in Rust, focusing on the use of the Result enum for recoverable errors and the panic! macro for unrecoverable errors. You'll learn how to propagate errors using the ? operator and how to design functions that can gracefully handle failure scenarios while maintaining Rust's safety and performance guarantees.

Prework

Prework Reading

Please read the following sections from The Rust Programming Language Book:

  • Chapter 9: Error Handling
  • Chapter 9.1: Unrecoverable Errors with panic!
  • Chapter 9.2: Recoverable Errors with Result

Pre-lecture Reflections

Before class, consider these questions:

  1. What are the differences between recoverable and unrecoverable errors in Rust?
  2. How does the Result enum facilitate error handling in Rust?
  3. What are the advantages of using the ? operator for error propagation?
  4. When should you use panic! versus returning a Result?
  5. How does Rust's approach to error handling compare to exception handling in other languages?

Lecture

Learning Objectives

By the end of this module, you will be able to:

  • Understand the difference between recoverable and unrecoverable errors
  • Use the panic! macro for handling unrecoverable errors
  • Use the Result enum for handling recoverable errors
  • Propagate errors using the ? operator
  • Design functions that can handle errors gracefully

Error Handling in Rust

Two basic options:

  • terminate when an error occurs: macro panic!(...)

  • pass information about an error: enum Result<T,E>

Macro panic!(...)

  • Use for unrecoverable errors
  • Terminates the application
fn divide(a:u32, b:u32) -> u32 {
    if b == 0 {
        panic!("I'm sorry, Dave. I'm afraid I can't do that.");
    }
    a/b
}

fn main() {
    println!("{}", divide(20,7));
    //println!("{}", divide(20,0));  // Try uncommenting this line
}

Enum Result<T,E>

Provided by the standard library, but shown here for reference.

#![allow(unused)]
fn main() {
enum Result<T,E> {
    Ok(T),
    Err(E),
}
}

Functions can use it to

  • return a result
  • or information about an encountered error
fn divide(a:u32, b:u32) -> Result<u32, String> {
    if b != 0 {
        Ok(a / b)
    } else {
        let str = format!("Division by zero {} {}", a, b);
        Err(str)
    }
}

fn main() {
    println!("{:?}", divide(20,7));
    println!("{:?}", divide(20,0));
}
  • Useful when the error best handled somewhere else
  • Example: input/output subroutines in the standard library

Common pattern: propagating errors

  • We are interested in the positive outcome: t in Ok(t)
  • But if an error occurs, we want to propagate it
  • This can be handled using match statements
fn divide(a:u32, b:u32) -> Result<u32, String> {
    if b != 0 {
        Ok(a / b)
    } else {
        let str = format!("Division by zero {} {}", a, b);
        Err(str)
    }
}

// compute a/b + c/d
fn calculate(a:u32, b:u32, c:u32, d:u32) -> Result<u32, String> {
    let first = match divide(a,b) {
        Ok(t) => t,
        Err(e) => return Err(e),
    };
    let second = match divide(c,d) {
        Ok(t) => t,
        Err(e) => return Err(e),
    };    
    Ok(first + second)
}


fn main() {
    println!("{:?}", calculate(16,4,18,3));
    println!("{:?}", calculate(16,0,18,3));
}

The question mark shortcut

  • Place ? after an expression that returns Result<T,E>

  • This will:

    • give the content of Ok(t)
    • or immediately return the error Err(e) from the encompassing function
fn divide(a:u32, b:u32) -> Result<u32, String> {
    if b != 0 {
        Ok(a / b)
    } else {
        let str = format!("Division by zero {} {}", a, b);
        Err(str)
    }
}

// compute a/b + c/d
fn calculate(a:u32, b:u32, c:u32, d:u32) -> Result<u32, String> {
    Ok(divide(a,b)? + divide(c,d)?)
}

fn main() {
    println!("{:?}", calculate(16,4,18,3));
    println!("{:?}", calculate(16,0,18,3));
}

Optional: try/catch pattern

  • In some languages we have the pattern try/catch or throw/catch or try/except (C++, Java, Javascript, Python).
  • Rust does not have something equivalent

The Rust pattern for error handling is the following:

    let do_steps = || -> Result<(), MyError> {
        do_step_1()?;
        do_step_2()?;
        do_step_3()?;
        Ok(())
    };

    if let Err(_err) = do_steps() {
        println!("Failed to perform necessary steps");
    }
  • Create a closure with the code you want to guard. Use the ? shorthand inside the closure for anything that can return an Error. Use a match or if let statement to catch the error.

Recap

  • Use panic! for unrecoverable errors
  • Use Result<T,E> for recoverable errors
  • Use ? to propagate errors