17 January 2023
In my last article, Rust: Panic, I talked about how to work with unrecoverable errors in Rust. What about recoverable errors? Those are much more common, after all.
Not surprisingly, Rust has a sophisticated system for error handling. At the heart of it is the Result type, defined as follows:
pub enum Result<T, E> {
Ok(T),
Err(E),
}
A function that might return an error should have
Result<T, E>
as its return type, where T is the type
that the function normally returns and E is a type that describes the
error if one occurs. There’s a convention that E should implement the std::error::Error
trait, but that’s not enforced by the type system.
What if your function doesn’t return anything in case of success? In
that case, you still use Result, with T set to the empty tuple
()
, also called “unit”.
Let’s take a look at a simple example. We’re using std::fs::read to read a file into memory:
use std::{fs, io};
fn read_file() {
let r: Result<Vec<u8>, io::Error> = fs::read("foo");
match r {
Ok(v) => {
println!("read {} bytes", v.len());
}
Err(e) => {
println!("got error: {}", e);
}
}
}
Result is just a regular enum, so can use match
to
distinguish its two variants. If we get an error we’ll just print it –
you can check the std::io::Error
documentation to verify that it implements the Display trait.
Quite often when a functions encounters an error, we just want to propagate it to the caller. Let’s change our read_file function so it returns the file size instead of printing it. In case of error, it’ll just return the error:
fn file_size() -> Result<usize, io::Error> {
let r: Result<Vec<u8>, io::Error> = fs::read("foo");
match r {
Ok(v) => Ok(v.len()),
Err(e) => Err(e),
}
}
Looks good, but it’s a little verbose. In Rust, the ? operator implements the “if this is an error, return the error” logic, so we can write the function like this:
fn file_size() -> Result<usize, io::Error> {
let v: Vec<u8> = fs::read("foo")?;
Ok(v.len())
}
That’s some sweet syntactic sugar! A single character takes care of
checking and correctly propagating any error that might occur. We can
make the code even shorter if we do without the v
variable:
fn file_size() -> Result<usize, io::Error> {
Ok(fs::read("foo")?.len())
}
The question mark followed by a dot may look a bit odd, but it makes
sense: the type of fs::read("foo")?
is
Vec<u8>
, and we’re calling the len
method on that vector.
The ? operator also works for Option: if you have a function that
returns Option<T>
and an expression e
of
type Option<U>
anywhere in that function, you can use
e?
to propagate None
.
Before we take another look at Result, we’ll first take a look at how you can define and return your own error types.
Let’s say you’re writing some kind of parser and you want to return a custom ParseError if the input can’t be parsed. We’ll keep it simple and define an error type with just an error message:
#[derive(Debug)]
struct ParseError {
message: String,
}
Following Rust conventions, we’ll also implement std::error::Error. If you take a look at the documentation, you’ll see that it has four methods, but all of them have default implementations. It also requires the Debug and Display traits. The Debug implementation is already derived, so the main thing we still need to do is to implement Display:
impl fmt::Display for ParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "parse error: {}", self.message)
}
}
impl std::error::Error for ParseError {}
Using the type is pretty straightforward. For a simple example, let’s
say we have a parse_boolean
function that recognizes
true
and false
keywords:
fn parse_boolean(input: &str) -> Result<bool, ParseError> {
match input {
"true" => Ok(true),
"false" => Ok(false),
_ => Err(ParseError {
message: format!("invalid boolean: {}", input),
}),
}
}
When you define an error type for a package or crate, it’s common to
just call it Error
. In this example we might have all the
parsing code in a package parsing
, so a type called
Error
would be parsing.Error
from outside the
package – probably a better choice than the redundant
parsing.ParseError
.
Result has quite a few methods that can be useful in various cases. Here are some of the more common ones, organized by when you’d use them:
Find out if it’s Ok or Err: The is_ok
and is_err
methods return a boolean:
let r: Result<u32, std::io::Error> = Ok(4);
println!("{}", r.is_ok()); // prints true
println!("{}", r.is_err()); // prints false
Just get the value: If you’re sure you can’t get an
error, you can call unwrap
to get the value out of the
Result:
let i: u32 = "123".parse().unwrap();
It’ll panic in case of error.
Use a default value: Call unwrap_or
to
replace errors with a default value. For example,
fs::read_to_string("foo").unwrap_or("<missing>".to_string())
returns either the contents of file foo
or the string
<missing>
.
Use a std::default::Default value: If type T
implements Default,
you can also call unwrap_or_default
.
fs::read_to_string("foo").unwrap_or_default()
returns either the contents of file foo
or an empty
string.
Change the value, leave the error: Sometimes you
have a Result<T, E>
where you want to modify the
T
value in the Ok
case, and just keep the
E
in the Err
case. Result
has
three methods to do that, depending on how you’re going to modify the
T:
and_then
for a function that returns a
Result<T, E>
map
for a function that returns a T
and
for a fixed Result
value.Here are some examples that download a file and then transform the filename in some way:
fn download() -> Result<String, std::io::Error> {
// download file and return filename...
}
fn download_and_read() -> Result<String, std::io::Error> {
download().and_then(|filename| fs::read_to_string(filename))
}
fn download_and_format() -> Result<String, std::io::Error> {
download().map(|filename| format!("download OK: {}", filename))
}
fn download_and_confirm() -> Result<String, std::io::Error> {
download().and(Ok("download OK".to_string()))
}
I have some more things I want to find out about Result, such as how it interacts with iterators. This article getting a bit long, though, so that’ll have to wait for another day…