Rust Errors Handling

Ignoring (unwrap)

Handy technic for making a prototypes quickly without slowing down on handling various corner cases. Most likely we will prepare all that is needed for POC to run and let us evaluate the idea.

Simple example, read from file specified as argument:

use std::{env, fs::File, io::Read};

fn main() {
    // assume file name will be set
    let file_name: String = env::args().nth(1).unwrap();
    // assume that file with provided name exists
    let mut file: File = File::open(file_name).unwrap();
    // reading file
    let mut str: String = String::new();
    file.read_to_string(&mut str).unwrap();
    // printing whatever was read
    print!("{}", str);
}

There is unwrap() used almost everywhere. When calling env::args().nth(1) we get Option<String> which means that desired argument might be missing but in our case we ignore this fact and assume that it will always be set. Similar thing with File::open(file_name) with one difference in result type Result<File, std::io::Error> this one explicitely tells us that operation can result in File or std::io::Err for whatever reason(file or permissions are missing etc). And again we ignore it and unwrap() ;). Same thing with file.read_to_string(&mut str). If some condition is missing - program will just crash.

This is how an error message will look like:

thread 'main' panicked at 'called `Option::unwrap()` on a `None` value', src/main.rs:5:48

Terminating (expect)

Another cheap approach is to use .expect("Woop") instead of unwrap. The program will crash in the same way as with unwrap() but will print specified text/reason describing what went wrong.

use std::{env, fs::File, io::Read};

fn main() {
    // assume file name will be set
    let file_name: String = env::args().nth(1).expect("Missing file name!");
    // assume that file with provided name exists
    let mut file: File = File::open(file_name).expect("Missing file");
    // reading file
    let mut str: String = String::new();
    file.read_to_string(&mut str).expect("Failed to read file");
    // printing whatever was read
    print!("{}", str);
}

So when running without file name argument, app will crash with such a message:

thread 'main' panicked at 'Missing file name!', src/main.rs:5:48

Propagation

Another nice approach is propagation of error to caller.

Simple example, there is a funtion for reading from file, which bubbles errors to caller function using Result<String, String> result type. Caller is aware that execution can result either in String data or String error message, therefore must bubble error to it’s own caller or handle the error inplace.

use std::env;

fn main() {
    let file_name: String = env::args().nth(1).unwrap();
    let maybe_data: Result<String, String> = read_file(&file_name);
    // Sort of dealing with the errors here ;)
    match maybe_data {
        Ok(data) => println!("Data: {}", data),
        Err(err) => eprintln!("Oops: {}", err),
    }
}

fn read_file(file_name: &str) -> Result<String, String> {
    Err(format!("Woo, failed to read a file: {}", file_name))
}

This is how it looks when failing:

Oops: Woo, failed to read a file: data.txt

Bubbling from deeper calls and using ? operator for automatic lifting:

use std::{env, fs::File};

fn main() {
    let file_name: String = env::args().nth(1).unwrap();
    let maybe_data: Result<String, String> = read_file(&file_name);
    match maybe_data {
        Ok(data) => println!("Data: {}", data),
        Err(err) => eprintln!("Oops: {}", err),
    }
}

fn read_file(file_name: &str) -> Result<String, String> {
    let file: File = open_file(file_name)?;
    Ok(String::from("some content"))
}

fn open_file(file_name: &str) -> Result<File, String> {
    Err(format!("Woo, failed to open file: {}", file_name))
}

Nicely propagated:

Oops: Woo, failed to open file: data.txt

Multiple errors

While using different libs, we would eventually need to handle multple error types or bubble them up to handle in more relevant place. To be able to do that, we use boxed error type Box<dyn std::error::Error>> for Result.

use std::{error::Error, io::ErrorKind};

fn main() {
    let maybe_data = multiple_errors();
    match maybe_data {
        Ok(data) => println!("Data: {}", data),
        Err(err) => eprintln!("Oops: {}", err),
    }
}

fn multiple_errors() -> Result<String, Box<dyn std::error::Error>> {
    perform_a()?;
    perform_b()?;
    Ok(String::from("some content"))
}

fn perform_a() -> Result<String, Box<dyn Error>> {
    // error created from string
    let custom_error = std::io::Error::new(ErrorKind::Other, "Woo, something went wrong!");
    // error can also be created from other error
    let custom_error2 = std::io::Error::new(ErrorKind::Interrupted, custom_error);
    Err(Box::new(custom_error2))
}

fn perform_b() -> Result<String, Box<dyn Error>> {
    let custom_error = std::io::Error::new(ErrorKind::AddrInUse, "Woo, so error!");
    Err(Box::new(custom_error))
}

When failing, error nicely propagates up to caller function and get handled or not ;)

Oops: Woo, something went wrong!

It is also possible to use map_err to convert/map error to our own error.

Custom errors

It is also possible to create out own errors to use them in our libraries for example. Error trait requires Debug and Display traits to be implemented for our custom errors.

use std::{error::Error, fmt, io::ErrorKind};

// deriving Debug is required by Error
#[derive(Debug)]
enum CustomError {
    FetchTimeout,
    InvalidData,
    SoDoge,
}

// implementing Error trait for our custom error
impl std::error::Error for CustomError {}

// implementing Display as Error requires
impl fmt::Display for CustomError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            CustomError::FetchTimeout => write!(f, "Timeout during fetch"),
            CustomError::InvalidData => write!(f, "Invalid data format"),
            CustomError::SoDoge => write!(f, "He he so doge"),
        }
    }
}

fn main() {
    let maybe_data = multiple_errors();
    match maybe_data {
        Ok(data) => println!("Data: {}", data),
        Err(err) => eprintln!("Oops: {}", err),
    }
}

fn multiple_errors() -> Result<String, Box<dyn std::error::Error>> {
    perform_c()?;
    perform_a()?;
    perform_b()?;
    Ok(String::from("some content"))
}

fn perform_a() -> Result<String, Box<dyn Error>> {
    // error created from string
    let custom_error = std::io::Error::new(ErrorKind::Other, "Woo, something went wrong!");
    // error can also be created from other error
    let custom_error2 = std::io::Error::new(ErrorKind::Interrupted, custom_error);
    Err(Box::new(custom_error2))
}

fn perform_b() -> Result<String, Box<dyn Error>> {
    let custom_error = std::io::Error::new(ErrorKind::AddrInUse, "Woo, so error!");
    Err(Box::new(custom_error))
}

fn perform_c() -> Result<String, CustomError> {
    Err(CustomError::SoDoge)
}

Our custom error also bubbles nicely to caller function!

Oops: He he so doge

Another nice thing about custom erros is that we can convert other errors to them via ? automatically while utilising ? operator. All that is required is From trait implementation for specified types.

impl From<std::io::Error> for CustomError {
    fn from(error: std::io::Error) -> Self {
        CustomError::SoDoge
    }
}