33
loading...
This website collects cookies to deliver better user experience
Option
and Result
types in Rust will be two of the most used types you will have at your disposal when writing your programs. Their concepts are simple but their use can be confusing at times for beginners. It was for me. This blog entry is an attempt to help explain how to use them effectively.Result
can also wrap a Rust error and this blog article will cover how to create those easily too.Option
type allows you to have a variable that may or may not contain a value. This is useful for passing optional parameters or as a return value from a function that may or may not succeed.enum Option<T> {
Some(T),
None,
}
Some
variant or no value at all via the None
variant.struct Option
{
int type;
union
{
struct Some
{
T t;
};
struct None {};
};
};
Option<&T>
is the same size as &T
. Effectively, this is like normal C pointers with the extra type safety.Option
here under the section titled 'Representation'.Option
for a generic function that returns the first item:fn first_item<T>(v: &Vec<T>) -> Option<T>
where T: Clone {
if v.len() > 0 {
Some(v[0].clone())
} else {
None
}
}
first_item
can only return a value if the vector being passed is not empty. This is a good candidate for Option
. If the vector is empty, we return None
, otherwise we return a copy of the value via Some
.None
variant forces the programmer to consider the case where the information required is not forthcoming.Result
is similar to Option
in that it can either return a value or it doesn't and is usually used as a return value from a function. But instead of returning a None
value, it returns an error value that hopefully encapsulates the information of why it went wrong.enum Result<T, E> {
Ok(T),
Err(E),
}
Ok
variant along with the final result of the function. However, if something fails within the function it can return an Err
variant along with the error value.use std::fs::File;
use std::io::{BufRead, BufReader, Error};
fn first_line(path: &str) -> Result<String, Error> {
let f = File::open(path);
match f {
Ok(f) => {
let mut buf = BufReader::new(f);
let mut line = String::new();
match buf.read_line(&mut line) {
Ok(_) => Ok(line),
Err(e) => Err(e),
}
}
Err(e) => Err(e),
}
}
std::fs::File::open
will return a Result<std::fs::File, std::io::Error>
. That is, it will either return a file handle if everything goes OK, or it will return an I/O error if it doesn't. We can match on this. If it's an error, we just return it immediately. Otherwise, we try to read the first line of that file via the std::io::BufReader
type. read_line
method returns a Result<String, std::io:Error>
and once again we match on this. If it was an error, we return it immediately. Notice that the error type for both the open
and read_line
methods is std::io::Error
. If they were different, this function wouldn't compile. We will deal with differing error types later.Ok
variant.?
that made handling errors less verbose. Basically, it turns code like this:let x = function_that_may_fail();
let value = match x {
Ok(v) => value,
Err(e) => return Err(e);
}
let value = function_that_may_fail()?;
?
operator changes the Result<T,E>
value into one of type T
. However, if the result was an error, the current function exits immediately with the same 'Err' variant. It unwraps the result if everything went OK, or it causes the function to return with an error if not.first_line
demo function above:use std::fs::File;
use std::io::{BufRead, BufReader, Error};
fn first_line(path: &str) -> Result<String, Error> {
let f = File::open(path)?;
let mut buf = BufReader::new(f);
let mut line = String::new();
buf.read_line(&mut line)?;
Ok(line)
}
Result
can be any type, like, for example, a String
. However, it is recommended to use a type that implements the trait std::error::Error
. By using this standard trait, users can handle your errors better and even aggregate them.Error
trait in all its glory:trait Error: Debug + Display {
fn source(&self) -> Option<&(dyn Error + 'static)> { None };
fn backtrace(&self) -> Option<&Backtrace> { None };
}
backtrace
method is only defined in the nightly version of the compiler, at time of writing, and so only source
is defined for the stable version. source
can be implemented to return an earlier error that this current error would be chained to. But if there is no previous error, None
is returned. Returning None
is the default implementation of this method.Error
must also implement Debug
and Display
traits.use std::fmt::{Result, Formatter};
use std::fs::File;
#[derive(Debug)]
enum MyError {
DatabaseNotFound(String),
CannotOpenDatabase(String),
CannotReadDatabase(String, File),
}
impl std::error::Error for MyError{}
impl std::fmt::Display for MyError {
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
match self {
Self::DatabaseNotFound(ref str) => write!(f, "File `{}` not found", str),
Self::CannotOpenDatabase(ref String) => write!(f, "Cannot open database: {}", str),
Self::CannotReadDatabase(ref String, _) => write!(f, "Cannot read database: {}", str),
}
}
}
derive
macro that implements the Debug
trait for us. Unfortunately, we cannot do that for Display
traits.Error
trait for compatibility with other error systems. Since we're not chaining errors, the default implementation will do.Display
trait, which is a requirement of the Error
trait.Result
's Err
variant can be tedious to write. Some consider the Error
trait lacking in functionality too. Various crates have been written to combat the boilerplate and to increase the usefulness of the types of error values you can generate.fn first_line(path: &str) -> Result<String, FirstLineError> { ... }
FirstLineError
in each of these crates. The basic foundation of the error will be this enum:enum FirstLineError {
CannotOpenFile { name: String },
NoLines,
}
failure
provides 2 major concepts: the Fail
trait and an Error
type.Fail
trait is a new custom error type specifically to hold better error information. This trait is used by libraries to define new error types.Error
trait is a wrapper around the Fail
types that can be used to compose higher-level errors. For example, a file open error can be linked to a database open error. The user would deal with the database open error, and could dig down further and obtain the original file error if they wanted.Fail
and crate users would interact with the Error
types.Failure
also supports backtraces if the crate feature backtrace
is enabled and the RUST_BACKTRACE
environment variable is set to 1
.FirstLineError
error type using this crate:use std::fs::File;
use std::io::{BufRead, BufReader};
use failure::Fail;
#[derive(Fail, Debug)]
enum FirstLineError {
#[fail(display = "Cannot open file `{}`", name)]
CannotOpenFile { name: String },
#[fail(display = "No lines found")]
NoLines,
}
fn first_line(path: &str) -> Result<String, FirstLineError> {
let f = File::open(path).map_err(|_| FirstLineError::CannotOpenFile {
name: String::from(path),
})?;
let mut buf = BufReader::new(f);
let mut line = String::new();
buf.read_line(&mut line)
.map_err(|_| FirstLineError::NoLines)?;
Ok(line)
}
Fail
and Display
traits automatically for us. It uses the fail
attributes to help it construct those traits.File::open
and BufRead::read_line
methods return a result based on the std::io::Error
type and not the FirstLineError
type that we require. We use the Result
's map_err
method to convert one error type to another.map_err
and other methods for Option
and Result
in my next blog article, but for now I will describe this one. If the result is an error, map_err
will call the closure given with the error value allowing us an opportunity to replace it with a different error value.File::open
returns a Result<(), std::io::Error
value. By calling map_err
we now return a Result<(), FirstLineError>
value. This is because the closure given returns a FirstLineError
value and through type inference, we get the new result type. If the result is an error, that closure will provide the value to associate with the Err
variant.File::open
is still a Result
type so we use the ?
operator to exit immediately if an error occurs.match first_line("foo.txt") {
Ok(line) => println!("First line: {}", line),
Err(e) => println!("Error occurred: {}", e),
}
Failure
can even allow you to create errors on the fly that are compatible with failure::Error
. For example,use failure::{ensure, Error};
fn check_even(num: i32) -> Result<(), Error> {
ensure!(num % 2 == 0, "Number is not even");
Ok(())
}
fn main() {
match check_even(41) {
Ok(()) => println!("It's even!"),
Err(e) => println!("{}", e),
}
}
Number is not even
as expected via the Display
trait of the error.failure
. format_err!
will create a string based error:let err = format_err!("File not found: {}", file_name);
format_err!
with a return:bail!("File not found: {}", file_name);
failure
but solves the issue where the actual error that occurred is not the error we want to report.map_err
to convert the std::io::Error
into one of our FirstLineError
variants. snafu
makes this easier by providing a context
method that allows the programmer to pass in the actual error they wish to report.snafu
:use std::fs::File;
use std::io::{BufRead, BufReader};
use snafu::{Snafu, ResultExt};
#[derive(Snafu, Debug)]
enum FirstLineError {
#[snafu(display("Cannot open file {} because: {}", name, source))]
CannotOpenFile {
name: String,
source: std::io::Error,
},
#[snafu(display("No lines found because: {}", source))]
NoLines { source: std::io::Error },
}
fn first_line(path: &str) -> Result<String, FirstLineError> {
let f = File::open(path).context(CannotOpenFile {
name: String::from(path),
})?;
let mut buf = BufReader::new(f);
let mut line = String::new();
buf.read_line(&mut line)
.context(NoLines)?;
Ok(line)
}
context()
, there needs to be a source
field in the variant. Notice that the enum type FirstLineError
is not included. We wrote CannotOpenFile
, not FirstLineError::CannotOpenFile
. And the source field is automatically set! There's some black magic going on there!source
for your underlying cause, you can rename it by marking the field you do want to be the source with #[snafu(source)]
. Also, if there is a field called source
that you don't want to be treated as snafu
's source field, mark it with #[snafu(source(false))]
.snafu
supports the backtrace
field too to store a backtrace at point of error. #[snafu(backtrace)]
et al. controls those fields like the source.ensure!
macro that functions like failure
's.anyhow::Result<T>
. This type can receive any error. It can create an ad-hoc error from a string using anyhow!
:let err = anyhow!("File not found: {}", file_name);
bail!
and ensure!
like other crates. anyhow
results can extend the errors using a context()
method:let err = anyhow!("File not found: {}", file_name)
.context("Tried to load the configuration file");
first_line
method implemented using anyhow
:use anyhow::Result;
#[derive(Debug)]
enum FirstLineError {
CannotOpenFile {
name: String,
source: std::io::Error,
},
NoLines {
source: std::io::Error,
},
}
impl std::error::Error for FirstLineError {}
impl std::fmt::Display for FirstLineError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
FirstLineError::CannotOpenFile { name, source } => {
write!(f, "Cannot open file `{}` because: {}", name, source)
}
FirstLineError::NoLines { source } => {
write!(f, "Cannot find line in file because: {}", source)
}
}
}
}
fn first_line(path: &str) -> Result<String> {
let f = File::open(path).map_err(|e| FirstLineError::CannotOpenFile {
name: String::from(path),
source: e,
})?;
let mut buf = BufReader::new(f);
let mut line = String::new();
buf.read_line(&mut line)
.map_err(|e| FirstLineError::NoLines { source: e })?;
Ok(line)
}
anyhow
doesn't define the Display
trait for us so we have to do that ourselves. Also map_err
has to come back if we want to convert error values from one domain to another. But, this time we use Result<String>
and we don't need to define which error is returned.anyhow
. It uses #[derive(thiserror::Error)]
to generate all the Display
and std::error::Error
boilerplate like other crates do.thiserror
makes it easier to chain lower-level errors using the #[from]
attribute. For example:#[derive(Error, Debug)]
enum MyError {
#[error("Everything blew up!")]
BlewUp,
#[error(transparent)]
IoError(#[from] std::io::Error)
}
std::io::Error
to MyError::IoError
.anyhow
for results, and thiserror
for errors:use std::fs::File;
use std::io::{BufRead, BufReader};
use anyhow::Result;
use thiserror::Error;
#[derive(Debug, Error)]
enum FirstLineError {
#[error("Cannot open file `{name}` because: {source}")]
CannotOpenFile {
name: String,
source: std::io::Error,
},
#[error("Cannot find line in file because: {source}")]
NoLines {
source: std::io::Error,
},
}
fn first_line(path: &str) -> Result<String> {
let f = File::open(path).map_err(|e| FirstLineError::CannotOpenFile {
name: String::from(path),
source: e,
})?;
let mut buf = BufReader::new(f);
let mut line = String::new();
buf.read_line(&mut line)
.map_err(|e| FirstLineError::NoLines { source: e })?;
Ok(line)
}
#[error(...)]
lines.main
to return a Result
. If main returns an Err
variant, it will return an error code other than 0 to the operating system (signifying a fail condition), and output the error using the Debug
trait.Display
trait, then you have to put your main function in another, then have your new main
call it and println!
the result:fn main() -> i32 {
if let Err(e) = run() {
println!("{}", e);
return 1;
}
return 0;
}
fn run() -> Result<(), Error> { ... }
Debug
trait is good enough for printing out your error, you can use:fn main() -> Result<(), Error> { ... }
Termination
trait:trait Termination {
fn report(self) -> i32;
}
report()
on the type you return from main
.Option
and Result
, like map_err
but this article is already too long. I will cover them next time.