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!