Rust has two ways of indicating errors:
- Returning a value
- Panicking
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...
Remember these? They are very important in Rust.
enum Option<T> {
Some(T),
None,
}
enum Result<T, E> {
Ok(T),
Err(E)
}
If you have an function where one outcome is "can't find it", we use Option
:
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.
When the result of a function is either Ok, or some Error value, we use Result
:
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.
You can handle Result
like any other enum
:
use std::io::prelude::*;
fn read_file(filename: &str) -> Result<String, std::io::Error> {
let mut file = match std::fs::File::open("data.txt") {
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)
}
It is idiomatic Rust to use ?
to handle errors.
use std::io::prelude::*;
fn read_file(filename: &str) -> Result<String, std::io::Error> {
let mut file = std::fs::File::open("data.txt")?;
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).
You can put anything in for the E
in Result<T, E>
:
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 }
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
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
An enum
is ideal to express one of a number of different kinds of thing:
/// 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,
}
An enum
can also hold data for each variant:
/// 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 Standard Library has a
trait
that yourenum Error
should implement - However, it's not easy to use
- Many people didn't bother
- See https://doc.rust-lang.org/std/error/trait.Error.html
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,
}
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(())
}
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
anyhow
if you do not care what error type your function returns, just that it captures something. - Use
thiserror
if you must design your own error types but want easyError
trait impl.
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::Err
into a panic with.unwrap()
or.expect("Oh no")