Error Handling

Trigger and handle irrecoverable panics

std cat-rust-patterns

The panic!(...) macro allows a program to terminate immediately and provide feedback to the caller of the program.

fn main() {
    panic!("Crash and burn");
}

panic! is closely tied with the unwrap method of both Option and Result enums. Both implementations call panic! when they are set to None or Err variants.

// use std::str::FromStr;

fn main() {
    let number_str = "42";

    // `parse()` attempts to convert the string into a number.
    // This operation can fail if the string isn't a valid number, so it returns
    // a `Result<u32, ParseIntError>`.
    let number: u32 = number_str.parse().unwrap();
    // `unwrap()` is called on the `Result` to extract the `u32` value.
    // - If the parsing is successful, the value is assigned to `number`.
    // - If the parsing fails, the program panics with an error message.

    println!("The number is: {}", number);
}

Provide a fallback value with unwrap_or_else

std cat-rust-patterns

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

fn main() {
    if !fs::exists("temp").unwrap() {
        fs::create_dir("temp").unwrap();
    }
    let _greeting_file = File::open("temp/hello.txt").unwrap_or_else(|error| {
        if error.kind() == ErrorKind::NotFound {
            File::create("temp/hello.txt").unwrap_or_else(|error| {
                panic!("Problem creating the file: {:?}", error);
            })
        } else {
            panic!("Problem opening the file: {:?}", error);
        }
    });
}

Return recoverable errors with Result

std cat-rust-patterns

use std::io;
use std::io::BufRead;

fn main() {
    let mut cursor = io::Cursor::new(b"foo\nbar");
    let mut buf = String::new();

    cursor
        // `read_line` puts whatever the user enters into the string we pass to it,
        // but it also returns a `Result` value.
        .read_line(&mut buf)
        // If this instance of `Result` is an `Err` value, expect will cause the program to crash
        // and display the message that you passed as an argument to expect.
        .expect("Failed to read line");

    // Alternative: `unwrap` panics if there is an error
    // let _greeting_file = std::fs::File::open("temp/hello.txt").unwrap();
}

Propagate errors with the ? operator

std cat-rust-patterns

use std::fs::File;
use std::io;
use std::io::Read;

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

fn main() {
    if !std::fs::exists("temp").unwrap() {
        std::fs::create_dir("temp").unwrap();
    }
    match read_username_from_file() {
        Ok(name) => println!("User name: {}", name),
        Err(err) => println!("Error: {}", err),
    }
}

If the value of the Result is an Ok, the value inside the Ok will get returned from this expression, and the program will continue. If the value is an Err, the Err will be returned from the whole function, as if we had used the return keyword, so the error value gets propagated to the calling code.

This error points out that we’re only allowed to use the ? operator in a function that returns Result, Option, or another type that implements std::ops::FromResidual⮳.

Another example:

use std::error::Error;

fn parse_port(s: &str) -> Result<u16, Box<dyn Error>> {
    // We need to use `Box<dyn Error>`, because the returned error type
    // cannot be determined during compile time: It will either
    // contain an instance of `std::num::ParseIntError` (from the parse
    // method, when parsing fails), or a string (when the port is
    // zero). Alternatively, you may use `anyhow::Result`.
    let port: u16 = s.parse()?;
    if port == 0 {
        Err(Box::from(format!("Invalid port: {}", port)))
    } else {
        Ok(port)
    }
}

fn main() {
    match parse_port("123") {
        Ok(port) => println!("Port: {}", port),
        Err(err) => panic!("{}", err),
    }
}

std::io defines the type alias type Result<T> = std::result::Result<T, std::io::Error>;

Handle errors correctly in main

cat-rust-patterns.

std::io::Error⮳ defined type implementing the std::error::Error⮳ trait.

The below recipe will tell how long the system has been running by opening the Unix file /proc/uptime and parse the content to get the first number. It returns the uptime, unless there is an error.

use std::fs::File;
use std::io::Read;

use anyhow::Result;
use anyhow::anyhow;

fn read_uptime() -> Result<u64> {
    let mut uptime = String::new();
    File::open("/proc/uptime")?.read_to_string(&mut uptime)?;

    Ok(uptime
        .split('.')
        .next()
        .ok_or(anyhow!("Cannot parse uptime data"))?
        .parse()?)
}

fn main() {
    match read_uptime() {
        Ok(uptime) => println!("uptime: {} seconds", uptime),
        Err(err) => eprintln!("error: {}", err),
    };
}

Avoid discarding errors during error conversions

reqwest reqwest-crates.io reqwest-github reqwest-lib.rs cat-wasm cat-web-programming::http-client cat-rust-patterns

Uses reqwest⮳::blocking⮳ to query a random integer generator web service. Converts the string response into an integer.


fn parse_response(
    response: reqwest::blocking::Response,
) -> anyhow::Result<u32> {
    let body = response.text()?;
    let body = body.trim();
    // println!("Body: {body}");
    let b = body.parse::<u32>()?;
    Ok(b)
}

fn main() -> anyhow::Result<()> {
    let url = "https://www.random.org/integers/?num=1&min=0&max=10&col=1&base=10&format=plain".to_string();
    let response = reqwest::blocking::get(url)?;
    let random_value: u32 = parse_response(response)?;
    println!("A random number between 0 and 10: {}", random_value);
    Ok(())
}

Obtain the backtrace in complex error scenarios

cat-rust-patterns

This recipe shows how to handle a complex error scenario and then print a backtrace. It relies on to extend errors by appending new errors.

The below recipes attempts to deserialize the value 256 into a u8⮳. An error will bubble up from Serde then csv and finally up to the user code.


// use std::fmt;

// use anyhow::anyhow;
// use anyhow::Context;
use anyhow::Result;
// use serde::Deserialize;

// #[derive(Debug, Deserialize)]
// struct Rgb {
//     red: u8,
//     blue: u8,
//     green: u8,
// }

// impl Rgb {
//     fn from_reader(csv_data: &[u8]) -> Result<Rgb> {
//         let c = csv::Reader::from_reader(csv_data)
//             .deserialize()
//             .nth(0)
//             .ok_or(anyhow!(""))?;
//         let color = c.context("")?;

//         Ok(color)
//     }
// }

// impl fmt::UpperHex for Rgb {
//     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
//         let hexa = u32::from(self.red) << 16
//             | u32::from(self.blue) << 8
//             | u32::from(self.green);
//         write!(f, "{:X}", hexa)
//     }
// }

// fn main() -> Result<()> {
//     let csv = "red,blue,green
// 102,256,204";

//     let rgb = Rgb::from_reader(csv.as_bytes())?;
//     println!("{:?} to hexadecimal #{:X}", rgb, rgb);

//     Ok(())
// }

fn main() -> Result<()> {
    Ok(())
}

Backtrace error rendered:

Error level - description
└> 0 - Cannot read CSV data
└> 1 - Cannot deserialize RGB color
└> 2 - CSV deserialize error: record 1 (line: 2, byte: 15): field 1: number too large to fit in target type
└> 3 - field 1: number too large to fit in target type

Run the recipe with RUST_BACKTRACE=1 to display a detailed backtrace associated with this error.