Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Error Handling in Rust

Demo

Example with anyhow

[package]
name = "with-anyhow"
version = "0.1.0"
edition = "2024"

[dependencies]
anyhow = "1.0.97"
use anyhow::bail;

fn main() -> anyhow::Result<()> {
    let args = std::env::args().collect::<Vec<_>>();
    if args.len() != 2 {
        bail!("Usage: {} <number>", args[0]);
    }
    let number: i32 = args[1].parse().map_err(|_| anyhow::anyhow!("Invalid number"))?;
    do_something(number)?;

    Ok(())
}


fn do_something(answer: i32) -> anyhow::Result<()> {
    println!("Doing something!");
    if answer != 42 {
        bail!("Invalid number: {}", answer);
    }

    println!("The answer is 42!");
    Ok(())
}
[package]
name = "with-anyhow"
version = "0.1.0"
edition = "2024"

[dependencies]
anyhow = "1.0.97"
use anyhow::bail;

fn main() {
    match do_something(42) {
        Ok(_) => println!("Success!"),
        Err(err) => println!("Error: {}", err),
    }

    match do_something(23) {
        Ok(_) => println!("Success!"),
        Err(err) => println!("Error: {}", err),
    }

}


fn do_something(answer: i32) -> anyhow::Result<()> {
    println!("Doing something!");
    if answer != 42 {
        bail!("An error occurred");
    }

    println!("The answer is 42!");
    Ok(())
}

TODO: Error handling in Rust

[package]
name = "error-handling"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
fn main() {
    let args = std::env::args().collect::<Vec<String>>();
    if args.len() != 2 {
        eprintln!("Usage: {} FILENAME", args[0]);
        std::process::exit(1);
    }
    let filename = &args[1];
    println!("{}", divide(filename));
}


fn divide(filename: &str) -> Result<i32, std::io::Error> {
    let content = std::fs::read_to_string(filename)?;
    let content = content.strip_suffix('\n').unwrap();
    let (dividend, divisor) = content.split_once(',').unwrap();
    let dividend = dividend.parse::<i32>().unwrap();
    let divisor = divisor.parse::<i32>().unwrap();
    dividend / divisor
}


// fn divide(filename: &str) -> i32 {
//     let content = std::fs::read_to_string(filename).unwrap();
//     let content = content.strip_suffix('\n').unwrap();
//     let (dividend, divisor) = content.split_once(',').unwrap();
//     let dividend = dividend.parse::<i32>().unwrap();
//     let divisor = divisor.parse::<i32>().unwrap();
//     dividend / divisor
// }
1
30,2
30,0
forty,two

Runtime error (panic) noticed during compilation

fn main() {
    let a = 8;
    let b = 2;
    let c = 0;
    let x = a / b;
    println!("{x}");

    #[allow(unconditional_panic)]
    let y = a / c;
    println!("{y}");
}
error: this operation will panic at runtime
 --> examples/errors/div_by_zero_hard_coded.rs:8:13
  |
8 |     let y = a / c;
  |             ^^^^^ attempt to divide `8_i32` by zero
  |
  = note: `#[deny(unconditional_panic)]` on by default

error: aborting due to previous error

Divide by zero panick in function

fn div(a: i32, b: i32) -> i32 {
    a / b
}

fn main() {
    for number in [2, 4, 0, 20] {
        let res = div(100, number);
        dbg!(res);
    }
}

Return error on failure

  • match
  • Ok
  • Err
fn div(a: i32, b: i32) -> Result<i32, &'static str> {
    if b == 0 {
        return Err("Cannot divide by 0");
    }
    Ok(a / b)
}

fn main() {
    for number in [2, 4, 0, 20] {
        let res = div(100, number);
        match res {
            Ok(result) => println!("{result}"),
            Err(err) => println!("{err}"),
        }
    }
}

Divide by zero runtime panic

use std::io;
use std::io::Write;

fn main() {
    loop {
        let dividend = 100;
        let divisor = get_number();
        let res = divide_by(dividend, divisor);
        println!("{dividend} / {divisor} = {res}");
    }
}

fn divide_by(dividend: i32, divisor: i32) -> i32 {
    dividend / divisor
}

fn get_number() -> i32 {
    let mut number = String::new();

    print!("Please type in a number: ");
    io::stdout().flush().expect("Oups");
    io::stdin()
        .read_line(&mut number)
        .expect("Failed to get input");

    let number: i32 = number.trim().parse().expect("Could not convert to i32");

    number
}
Please type in a number: 20
5
Please type in a number: 0
thread 'main' panicked at 'attempt to divide by zero', examples/errors/div_by_zero.rs:7:13
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

Divide by zero return error

use std::io;
use std::io::Write;

fn main() {
    loop {
        let dividend = 100;
        let divisor = get_number();
        match divide_by(dividend, divisor) {
            Ok(val) => println!("{dividend} / {divisor} = {val}"),
            Err(error) => println!("Error {:?}", error),
        };
    }
}

fn divide_by(dividend: i32, divisor: i32) -> Result<i32, &'static str> {
    if divisor == 0 {
        return Err("Cannot divide by 0");
    }
    let res = dividend / divisor;
    Ok(res)
}

fn get_number() -> i32 {
    let mut number = String::new();

    print!("Please type in a number: ");
    io::stdout().flush().expect("Oups");
    io::stdin()
        .read_line(&mut number)
        .expect("Failed to get input");

    let number: i32 = number.trim().parse().expect("Could not convert to i32");

    number
}

factorial function, runtime panic

  • panic!

  • In this series of examples we'll see how a function we write can report error and how the caller can handle the error.

  • In the first example we implement a function to calculate factorial recursively. The specific task is not important, just that if the user supplies a negative number then this code will crash. More precisely, it will have a panic when our program reaches stack overflow.

fn main() {
    for number in [5, 10, 0, -1, 3] {
        let fact = factorial(number);
        println!("{number}! is {fact}");
    }
}

fn factorial(n: i64) -> i64 {
    if n == 0 {
        return 1;
    }
    n * factorial(n - 1)
}
Please type in a number: 5
5! is 120
Please type in a number: 10
10! is 3628800
Please type in a number: -1

thread 'main' has overflowed its stack
fatal runtime error: stack overflow
./rust.sh: line 19: 13666 Aborted                 (core dumped) ./myexe $*

factorial create panic

In order to avoid this stack overflow our function needs to check the input and then if it is a negative number then report it. (I know, instead of i64 we could have used u64 which is an unsigned integer that would only allow non-negative numbers. but in other functions we might not be able to use the type-system for that. e.g. what if we expect a positive whole number larger than 2?

One way to avoid reaching stack overflow is to call panic! ourselves.

fn main() {
    for number in [5, 10, 0, -1, 3] {
        let fact = factorial(number);
        println!("{number}! is {fact}");
    }
}

fn factorial(n: i64) -> i64 {
    if n < 0 {
        panic!("Cannot compute factorial of negative number");
    }
    if n == 0 {
        return 1;
    }
    n * factorial(n - 1)
}
Please type in a number: 10
10! is 3628800
Please type in a number: 0
0! is 1
Please type in a number: -1
thread 'main' panicked at 'Cannot compute factorial of negative number', examples/errors/factorial_create_panic.rs:14:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

Out of bound for vectors

  • Run-time panic
#[allow(clippy::useless_vec)]
fn main() {
    let v = vec!["apple", "banana", "peach"];
    println!("{}", v[0]);
    println!("{}", v[5]);
}
apple
thread 'main' panicked at 'index out of bounds: the len is 3 but the index is 5', examples/errors/out_of_bounds_vector.rs:4:20
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

Out of bound for arrays

  • Compile-time panic
fn main() {
    let v = ["apple", "banana", "peach"];
    println!("{}", v[0]);
    println!("{}", v[5]);
}
error: this operation will panic at runtime
 --> examples/errors/out_of_bounds_array.rs:4:20
  |
4 |     println!("{}", v[5]);
  |                    ^^^^ index out of bounds: the length is 3 but the index is 5
  |
  = note: `#[deny(unconditional_panic)]` on by default

error: aborting due to previous error

Open files

use std::fs::File;

fn main() {
    let filename = "rust.json";
    let res = File::open(filename);
    println!("{:?}", res);

    //let filename = "hello.txt";
    //let res = File::open(filename);
    //println!("{:?}", res);

    match res {
        Ok(file) => println!("{:?}", file),
        Err(error) => println!("problem {:?}", error),
    }
}

Error handling on the command line

  • cmp
  • Ordering
  • std::cmp::Ordering
use std::cmp::Ordering;
use std::env;
use std::process::exit;

fn main() {
    run();
}

// We are expecting the user to provide a command line argument.
// How do we handle if the user does not provide it (or provides too many)?

// In the get_name function call exit if the user did not supply the required parameter
// fn run() {
//     let name = get_name();
//     println!("{}", name);
// }

// fn get_name() -> String {
//     let args: Vec<String> = env::args().collect();
//     if args.len() != 2 {
//         eprintln!("Usage: {} NAME", args[0]);
//         exit(1);
//     }
//     args[1].clone()
// }

// Return the empty string
// This makes the caller need to handle the cases, but more problematic is that in the called
// we can't tell if there were to many or too few (or otherwise incorrect) parameters
// fn run() {
//     let name = get_name();
//     if name == "" {
//         println!("Got empty string")
//     } else {
//         println!("{}", name);
//     }
// }
// fn get_name() -> String {
//     let args: Vec<String> = env::args().collect();
//     if args.len() == 2 { args[1].clone() } else { String::new() }
// }

// Return either the Ok() with the value provided by the user or Err with the specific error message
// The caller needs to use match to sepearat the two cases.
// fn run() {
//     let res = get_name();
//     //println!("{:?}", res);
//     match res {
//         Err(error_message) => println!("{error_message}"),
//         Ok(name) => println!("{name}"),
//     }
// }

fn run() {
    let res = get_name();
    //println!("{:?}", res);
    let name = match res {
        Err(error_message) => {
            eprintln!("{error_message}");
            exit(1);
        }
        Ok(name) => name,
    };
    println!("{name}");
}

fn get_name() -> Result<String, String> {
    let args: Vec<String> = env::args().collect();
    match args.len().cmp(&2_usize) {
        Ordering::Less => Err(String::from("Not enough parameters")),
        Ordering::Greater => Err(String::from("Too many parameters")),
        Ordering::Equal => Ok(args[1].clone()),
    }
}

main returning Result - Set exit code

  • Result
  • Box
  • dyn
  • Error
  • Ok
  • ?
#![allow(unused_variables)]

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let args = std::env::args().collect::<Vec<_>>();

    let filename = &args.get(1).ok_or("Missing file")?;
    let number = args.get(2).ok_or("Param 2 was missing")?.parse::<u8>()?;
    let num = number.checked_add(255).ok_or("overflow")?;

    let content = std::fs::read_to_string(filename)?;

    Ok(())
}

A number of invocations:

cargo run
cargo run data.txt
cargo run data.txt 10
cargo run data.txt abc
cargo run data.txt 0

Error in internal function

  • Result
  • Box
  • dyn
  • Error
  • Ok
  • ?
#![allow(unused_variables)]

fn main() {
    let res = run();
    match res {
        Ok(_val) => println!("Ok"),
        Err(err) => println!("Error: {err}"),
    }
}

fn run() -> Result<(), Box<dyn std::error::Error>> {
    read_file("Cargo.toml")?;
    //read_file("a.txt")?;
    convert_number("23")?;
    //convert_number("hello")?;
    increment_number(0)?;
    //increment_number(10)?;

    Ok(())
}

fn read_file(filename: &str) -> Result<String, Box<dyn std::error::Error>> {
    let content = std::fs::read_to_string(filename)?;

    Ok(content)
}

fn convert_number(text: &str) -> Result<u8, Box<dyn std::error::Error>> {
    let number = text.parse::<u8>()?;

    Ok(number)
}

fn increment_number(number: u8) -> Result<u8, Box<dyn std::error::Error>> {
    let num = number.checked_add(255).ok_or("overflow")?;

    Ok(num)
}