Skip to content
Published at:

Rust Error Handling

Topic

  • Quick overview of the functionality
  • Very deep topic
  • Helpful libraries: anyhow, thiserror

Example:

rust
use std::{
    fs::File,
    io::{Error, Read},
    process::abort,
};

fn main() {
    let file: Result<File, Error> = File::open("input.txt");
    match file {
        Ok(mut file) => {
            let mut s = String::new();
            let res: Result<usize, Error> = file.read_to_string(&mut s);
            match res {
                Ok(_) => {
                    println!("{}", s);
                }
                Err(e) => {
                    eprintln!("Error reading file: {}", e);
                    abort();
                }
            }
        }
        Err(e) => {
            eprintln!("Error opening file: {}", e);
            abort();
        }
    }
}

Imporve with return

  • Can return a Result for main
  • Imporoves nesting problem
  • We lose nice context message(we'll fix that later)
  • Still tedious...
rust
use std::{
    fs::File,
    io::{Error, Read},
};

fn main() -> Result<(), Error> { 
    let file: Result<File, Error> = File::open("input.txt");

    let mut file = match file {
        Ok(file) => file,
        Err(e) => return Err(e),
    };

    let mut s = String::new();
    let res: Result<usize, Error> = file.read_to_string(&mut s);
    let _size = match res {
        Ok(size) => size,
        Err(e) => return Err(e),
    };

    println!("{}", s);
    Ok(())
}

The try ? operator

  • ? operator follows any expression
  • Handles the short-circuit logic we just implemented
rust
use std::{
    fs::File,
    io::{Error, Read},
};

fn main() -> Result<(), Error> {
    let mut file: File = File::open("input.txt")?;
    let mut s = String::new();
    let _res: usize = file.read_to_string(&mut s)?;
    println!("{}", s);
    Ok(())
}

Custom error types

  • Libraries may return different types fo errors
  • Need to "unify" them somehow
  • May also want to include context information
  • We can do this manually...
rust
use std::{fs::File, io::Read};

type Result<T, E = CustomError> = std::result::Result<T, E>;

#[derive(Debug)]
enum CustomError {
    OpenFile {
        filename: String,
        source: std::io::Error,
    },
    ReadToString(std::io::Error),
}

fn main() -> Result<()> {
    let mut file: File = File::open("input.txt").map_err(|e| CustomError::OpenFile {
        filename: "input.txt".to_string(),
        source: e,
    })?;
    let mut s = String::new();
    let _res: usize = file.read_to_string(&mut s).map_err(CustomError::ReadToString)?;
    println!("{}", s);
    Ok(())
}

Thiserror

  • Helper library for generating these types
  • Handles easier display, wrapping context
rust
use std::{fs::File, io::Read};
use thiserror::Error;

type Result<T, E = CustomError> = std::result::Result<T, E>;

#[derive(Error, Debug)]
enum CustomError {
    #[error("Could not open file {filename}; error: {source}")]
    OpenFile {
        filename: String,
        source: std::io::Error,
    },
    #[error("Could not read file to string; error: {0}")]
    ReadToString(std::io::Error),
}

fn main() -> Result<()> {
    let mut file: File = File::open("input.txt").map_err(|e| CustomError::OpenFile {
        filename: "input.txt".to_string(),
        source: e,
    })?;
    let mut s = String::new();
    let _res: usize = file
        .read_to_string(&mut s)
        .map_err(CustomError::ReadToString)?;
    println!("{}", s);
    Ok(())
}

From trait approach

  • ? operator automatically uses into (part of From)
  • Doesn't work well for grabbing context
rust
use std::{fs::File, io::Read};
use thiserror::Error;

type Result<T, E = CustomError> = std::result::Result<T, E>;

#[derive(Error, Debug)]
enum CustomError {
    #[error("File is empty")]
    EmptyFile,
    #[error("Could not read file to string; error: {0}")]
    IOError(std::io::Error),
}

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

fn main() -> Result<()> {
    let mut file: File = File::open("input.txt")?; 
    let mut s = String::new();
    let _res: usize = file.read_to_string(&mut s)?; 
    println!("{}", s);
    Ok(())
}

The anyhow crate

  • Uses a trait object (not something we covered)
  • Cannot see the different ways things can fail
  • Great for applications
  • Doesn't force context information
rust
use anyhow::{ensure, Context, Result};
use std::{fs::File, io::Read};

fn main() -> Result<()> {
    let filename = "input.txt";
    let mut file: File =
        File::open(filename).with_context(|| format!("Could not open file {}", filename))?; 

    let mut s = String::new();
    let _res: usize = file
        .read_to_string(&mut s)
        .context("Could not read file to string")?; 

    ensure!(!s.is_empty(), "File is empty");
    println!("{}", s);
    Ok(())
}

Iterator of Results

  • Some Iterators may generate errors(e.g, lines)
  • Common pattern: let foo = foo?;
rust
use anyhow::{Context, Result};
use std::{
    fs::File,
    io::{BufRead, BufReader},
};

fn main() -> Result<()> {
    let filename = "input.txt";
    let mut file: File =
        File::open(filename).with_context(|| format!("Could not open file {}", filename))?;

    let file = BufReader::new(file);
    for line in file.lines() {
        // let line = line.context("Reading a line from the file")?;
        let line = line?; 
        println!("{}", line);
    }
    Ok(())
}

Conclusion

  • ? operator is awesome
  • Return Result from functions that may fail
  • Use a helper library to handle your error types
  • Decide if you want explicit failure case (thiserror) or "something went wrong"(anyhow)
  • Include context information!

References:

Updated at: