match

In Rust we use the match expression to check for errors in the Result:

use std::fs::File;
 
fn main() {
    let f = File::open("hello.txt");
 
    let f = match f {
        Ok(file) => file,
        Err(error) => panic!("Problem opening the file: {:?}", error),
    };
}

Generally speaking you want to perform different actions depending on the error type:

use std::fs::File;
use std::io::ErrorKind;
 
fn main() {
    let f = File::open("hello.txt");
 
    let f = match f {
        Ok(file) => file,
        Err(error) => match error.kind() {
            ErrorKind::NotFound => match File::create("hello.txt") {
                Ok(fc) => fc,
                Err(e) => panic!("Problem creating the file: {:?}", e),
            },
            other_error => panic!("Problem opening the file: {:?}", other_error),
        },
    };
}

Ditching match altogether

A more elegant way of writing the above:

use std::fs::File;
use std::io::ErrorKind;
 
fn main() {
    let f = File::open("hello.txt").unwrap_or_else(|error| {
        if error.kind() == ErrorKind::NotFound {
            File::create("hello.txt").unwrap_or_else(|error| {
                panic!("Problem creating the file: {:?}", error);
            })
        } else {
            panic!("Problem opening the file: {:?}", error);
        }
    });
}

Panic on Error shortcuts

unwrap

If the Result value is Ok, unwrap will return the value inside the Ok. If the Result is of the Err variant, unwrap will call the panic! macro

use std::fs::File;
 
fn main() {
    let f = File::open("hello.txt").unwrap();
}

expect

expect is almost the same as unwrap, the only difference being that it allws us to choose the panic! error message:

use std::fs::File;
 
fn main() {
    let f = File::open("hello.txt").expect("Failed to open hello.txt");
}

Propagating Errors

When you’re writing a function whose implementation calls something that might fail, instead of handling the error within this function, you can return the error to the calling code so that it can decide what to do. This is known as propagating the error and gives more control to the calling code, where there might be more information or logic that dictates how the error should be handled than what you have available in the context of your code.

#![allow(unused)]
fn main() {
    use std::fs::File;
    use std::io;
    use std::io::Read;
 
    fn read_username_from_file() -> Result<String, io::Error> {
        let f = File::open("hello.txt");
 
        let mut f = match f {
            Ok(file) => file,
            Err(e) => return Err(e),
        };
 
        let mut s = String::new();
 
        match f.read_to_string(&mut s) {
            Ok(_) => Ok(s),
            Err(e) => Err(e),
        }
    }
}

This can of course be refactored to something nicer:

#![allow(unused)]
fn main() {
    use std::fs::File;
    use std::io;
    use std::io::Read;
 
    fn read_username_from_file() -> Result<String, io::Error> {
        let mut f = File::open("hello.txt")?;
        let mut s = String::new();
        f.read_to_string(&mut s)?;
        Ok(s)
    }
}

And this can be refactored even more:

#![allow(unused)]
fn main() {
    use std::fs::File;
    use std::io;
    use std::io::Read;
 
    fn read_username_from_file() -> Result<String, io::Error> {
        let mut s = String::new();
 
        File::open("hello.txt")?.read_to_string(&mut s)?;
 
        Ok(s)
    }
}