Error Handling

Irrecoverable panics

cat-rust-patterns

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

Recoverable errors with Result

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();
}

unwrap_or_else

cat-rust-patterns


## A Shortcut for propagating errors: the ? Operator {#question-mark-operator}

[![cat-rust-patterns][cat-rust-patterns-badge]][cat-rust-patterns]<a name="a004"></a>

```rust
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() {
    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>> {
    // needed 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).
    let port: u16 = s.parse()?;
    if port == 0 {
        Err(Box::from(format!("invalid: {}", 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

Handles error that occur when trying to open a file that does not exist. It is achieved by using error_chain⮳, a library that takes care of a lot of boilerplate code needed in order to handle errors in Rust⮳.

Io(std::io::Error) inside foreign_links⮳ allows automatic conversion from std::io::Error⮳ into error_chain::error_chain⮳ 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. Returns uptime unless there is an error.

Other recipes in this book will hide the error_chain⮳ boilerplate, and can be seen by expanding the code with the ⤢ button.

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

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

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

error_chain cat-rust-patterns

The error_chain⮳ crate makes matching⮳ on different error types returned by a function possible and relatively compact. error_chain::example_generated::ErrorKind⮳ determines the error type.

Uses reqwest⮳::blocking⮳ to query a random integer generator web service. Converts the string response into an integer. The Rust standard library, reqwest⮳, and the web service can all generate errors. Well defined Rust errors use foreign_links⮳ An additional error_chain::example_generated::ErrorKind⮳ variant for the web service error uses errors block of the error_chain! macro.


fn parse_response(
    response: reqwest::blocking::Response,
) -> anyhow::Result<u32> {
    let body = response.text()?;
    let body = body.trim();
    // println!("{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 backtrace of complex error scenarios

error_chain cat-rust-patterns

This recipe shows how to handle a complex error scenario and then print a backtrace. It relies on chain_err⮳ to extend errors by appending new errors. The error stack can be unwound, thus providing a better context to understand why an error was raised.

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.

// TODO rewrite

// 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() {}

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 error_chain::ChainedError::backtrace⮳ associated with this error.