Error Handling
There are no exceptions
Rust has two ways of indicating errors:
- Returning a value
- Panicking
Returning a value
fn parse_header(data: &str) -> bool {
if !data.starts_with("HEADER: ") {
return false;
}
true
}
It would be nice if we could return data as well as ok, or error...
Foretold enums strike back! 🤯
Remember these? They are very important in Rust.
#![allow(unused)] fn main() { enum Option<T> { Some(T), None, } enum Result<T, E> { Ok(T), Err(E) } }
 
For now, think of T and E as placeholders for your own types. These are also called
generics and there will be a dedicated chapter for them.
I can't find it
If you have an function where one outcome is "can't find it", we use Option:
#![allow(unused)] fn main() { fn parse_header(data: &str) -> Option<&str> { if !data.starts_with("HEADER: ") { return None; } Some(&data[8..]) } }
Note:
It's so important, it is special-cased within the compiler so you can say None instead of Option::None, as you would with any other enum.
That's gone a bit wrong
When the result of a function is either Ok, or some Error value, we use Result:
#![allow(unused)] fn main() { enum MyError { BadHeader } // Need to describe both the Ok type and the Err type here: fn parse_header(data: &str) -> Result<&str, MyError> { if !data.starts_with("HEADER: ") { return Err(MyError::BadHeader); } Ok(&data[8..]) } }
Note:
It's so important, it is special-cased within the compiler so you can say Ok and Err instead of Result::Ok and Result::Err, as you would with any other enum.
Handling Results by hand
You can handle Result like any other enum:
#![allow(unused)] fn main() { use std::io::prelude::*; fn read_file(filename: &str) -> Result<String, std::io::Error> { let mut file = match std::fs::File::open(filename) { Ok(f) => f, Err(e) => { return Err(e); } }; let mut contents = String::new(); if let Err(e) = file.read_to_string(&mut contents) { return Err(e); } Ok(contents) } }
Handling Results with ?
It is idiomatic Rust to use ? to let the caller handle errors while continuing
for the regular happy path.
#![allow(unused)] fn main() { use std::io::prelude::*; fn read_file(filename: &str) -> Result<String, std::io::Error> { let mut file = std::fs::File::open(filename)?; let mut contents = String::new(); file.read_to_string(&mut contents)?; Ok(contents) } }
Note:
This was added in Rust 1.39.
The ? operator will evaluate to the Ok value if the Result is Ok, and it will cause an early return with the error value if it is Err. It will also call .into() to perform a type conversion if necessary (and if possible).
The ? operator allows exception like behaviour: Errors can be bubbled up, similar to exceptions.
What kind of Error?
You can put anything in for the E in Result<T, E>:
#![allow(unused)] fn main() { fn literals() -> Result<(), &'static str> { Err("oh no") } fn strings() -> Result<(), String> { Err(String::from("oh no")) } fn enums() -> Result<(), Error> { Err(Error::BadThing) } enum Error { BadThing, OtherThing } }
Using String Literals as the Err Type
Setting E to be &'static str lets you use "String literals"
- It's cheap
- It's expressive
- But you can't change the text to include some specific value
- And your program can't tell what kind of error it was
Using Strings as the Err Type
Setting E to be String lets you make up text at run-time:
- It's expressive
- You can render some values into the
String - But it costs you a heap allocation to store the bytes for the
String - And your program still can't tell what kind of error it was
Using enums as the Err Type
An enum is ideal to express one of a number of different kinds of thing:
#![allow(unused)] fn main() { /// Represents the ways this module can fail enum Error { /// An error came from the underlying transport Io, /// During an arithmetic operation a result was produced that could not be stored NumericOverflow, /// etc DiskFull, /// etc NetworkTimeout, } }
Enum errors with extra context
An enum can also hold data for each variant:
#![allow(unused)] fn main() { /// Represents the ways this module can fail enum Error { /// An error came from the underlying transport Io(std::io::Error), /// During an arithmetic operation a result was produced that could not /// be stored NumericOverflow, /// Ran out of disk space DiskFull, /// Remote system did not respond in time NetworkTimeout(std::time::Duration), } }
The std::error::Error trait
- The Standard Library has a
traitthat yourenum Errorshould implement - However, it's not easy to use
- Many people didn't bother
- See https://doc.rust-lang.org/std/error/trait.Error.html
Helper Crates
So, people created helper crates like thiserror
use thiserror::Error;
#[derive(Error, Debug)]
pub enum DataStoreError {
#[error("data store disconnected")]
Disconnect(#[from] io::Error),
#[error("the data for key `{0}` is not available")]
Redaction(String),
#[error("invalid header (expected {expected:?}, found {found:?})")]
InvalidHeader { expected: String, found: String },
#[error("unknown data store error")]
Unknown,
}
Something universal
Exhaustively listing all the ways your dependencies can fail is hard.
One solution:
fn main() -> Result<(), Box<dyn std::error::Error>> { let _f = std::fs::File::open("hello.txt")?; // IO Error let _s = std::str::from_utf8(&[0xFF, 0x65])?; // Unicode conversion error Ok(()) }
Anyhow
The anyhow crate gives you a nicer type:
fn main() -> Result<(), anyhow::Error> {
let _f = std::fs::File::open("hello.txt")?; // IO Error
let _s = std::str::from_utf8(&[0xFF, 0x65])?; // Unicode conversion error
Ok(())
}
Note:
- Use
anyhowif you do not care what error type your function returns, just that it captures something. This oftentimes applies to applications. - Use
thiserrorif you must design your own error types but want easyErrortrait impl. This oftentimes applies to libraries.
Result value conversions
#![allow(unused)] fn main() { struct OhNoError; struct ThisIsBadError; fn result_u32() -> Result<u32, OhNoError> { Ok(1) } fn result_but_its_u64() -> Result<u64, OhNoError> { // `map` expects a function which maps the value to another value. // It takes any function which implements [core::ops::FnOnce]. result_u32().map(|v| v as u64) } fn result_u32_but_error_is_bad() -> Result<u32, ThisIsBadError> { // `map_err` expects a function which maps the error to another error. // It takes any function which implements [core::ops::FnOnce]. result_u32().map_err(|_| ThisIsBadError) } }
Convert Result and Option into each other
#![allow(unused)] fn main() { struct OhNoError; fn option_to_result() -> Result<u32, OhNoError> { let option = None; option.ok_or(OhNoError) } fn result_to_option() -> Option<u32> { let result: Result<u32, OhNoError> = Ok(2); result.ok() } }
Convert Option::None or Result::Err(E) to value
#![allow(unused)] fn main() { struct OhNoError; fn none_becomes_be_zero() -> u32 { let opt_val: Option<u32> = None; opt_val.unwrap_or(0) } fn error_becomes_zero() -> u32 { let fail_val: Result<u32, OhNoError> = Err(OhNoError); fail_val.unwrap_or(0) } }
There is more
ResultandOptionhave a lot more methods available!ResultdocumenationOptiondocumentation- These methods can reduce a lot of boilerplate code, especially when
combined with the
FromandIntovalue conversion traits.
Note:
- Example for combining this with
From/Into: Mapping a child error into a parent error can be simply achieved by usingchild_err.map_err(|e| e.into())as long aFrom<ChildError>is implemented forParentError
Panicking
The other way to handle errors is to generate a controlled, program-ending, failure.
- You can
panic!("x too large ({})", x); - You can call an API that panics on error (like indexing, e.g.
s[99]) - You can convert a
Result::Errinto a panic with.unwrap()or.expect("Oh no")