Rust dyn error

The example projects in The Rust Programming Language are great for introducing new would-be Rustacea...

The example projects in The Rust Programming Language are great for introducing new would-be Rustaceans to different aspects and features of Rust. In this post, we’ll be looking at some different ways of implementing a more robust error handling infrastructure by extending the minigrep project from The Rust Programming Language.

The minigrep project is introduced in chapter 12 and walks the reader through building a simple version of the grep command line tool, which is a utility for searching through text. For example, you’d pass in a query, the text you’re searching for, along with the file name where the text lives, and get back all of the lines that contain the query text.

The goal of this post is to extend the book’s minigrep implementation with more robust error handling patterns so that you’ll have a better idea of different ways to handle errors in a Rust project.

For reference, you can find the final code for the book’s version of minigrep here.

Error handling use cases

A common pattern when it comes to structuring Rust projects is to have a «library» portion where the primary data structures, functions, and logic live and an «application» portion that ties the library functions together.

You can see this in the file structure of the original minigrep code: the application logic lives inside of the src/bin/main.rs file, and it’s merely a thin wrapper around data structures and functions that are defined in the src/lib.rs file; all the main function does is call minigrep::run.

This is important to point out because depending on whether we’re building an application or a library changes how we approach error handling.

When it comes to an application, the end user most likely doesn’t want to know about the nitty gritty details of what caused an error. Indeed, the end user of an application probably only ought to be notified of an error in the event that the error is unrecoverable. In this case, it’s also useful to provide details on why the unrecoverable error occurred, especially if it has to do with user input. If some sort of recoverable error happened in the background, the consumer of an application probably doesn’t need to know about it.

Conversely, when it comes to a library, the end users are other developers who are using the library and building something on top of it. In this case, we’d like to give as many relevant details about any errors that occurred in our library as possible. The consumer of the library will then decide how they want to handle those errors.

So how do these two approaches play together when we have both a library portion and an application portion in our project? The main function executes the minigrep::run function and outputs any errors that crop up as a result. So most of our error handling efforts will be focused on the library portion.

Surfacing library errors

In src/lib.rs, we have two functions, Config::new and run, which might return errors:

impl Config {
    pub fn new(mut args: env::Args) -> Result<Config, &'static str> {
        args.next();

    let query = match args.next() {
        Some(arg) => arg,
        None => return Err("Didn't get a query string"),
    };

    let filename = match args.next() {
        Some(arg) => arg,
        None => return Err("Didn't get a file name"),
    };

    let case_sensitive = env::var("CASE_INSENSITIVE").is_err();

    Ok(Config {
        query,
        filename,
        case_sensitive,
    })
}

pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
    let contents = fs::read_to_string(config.filename)?;

    let results = if config.case_sensitive {
        search(&config.query, &contents)
    } else {
        search_case_insensitive(&config.query, &contents)
    };

    for line in results {
        println!("{}", line);
    }

    Ok(())
}

Enter fullscreen mode

Exit fullscreen mode

There are exactly three spots where errors are being returned: two errors occur in the Config::new function, which returns a Result<Config, &'static str>. In this case, the error variant of the Result is a static string slice.

Here we return an error when a query is not provided by the user.

let query = match args.next() {
    Some(arg) => arg,
    None => return Err("Didn't get a query string"),
};

Enter fullscreen mode

Exit fullscreen mode

Here we return an error when a filename is not provided by the user.

let filename = match args.next() {
    Some(arg) => arg,
    None => return Err("Didn't get a file name"),
};

Enter fullscreen mode

Exit fullscreen mode

The main problem with structuring our errors in this way as static strings is that the error messages are not located in a central spot where we can easily refactor them should we need to. It also makes it more difficult to keep our error messages consistent between the same types of errors.

The third error occurs at the top of run function, which returns a Result<(), Box<dyn Error>>. The error variant in this case is a trait object that implements the Error trait. In other words, the error variant for this function is any instance of a type that implements the Error trait.

Here we bubble up any errors that might have occurred as a result of calling fs::read_to_string.

let contents = fs::read_to_string(config.filename)?;

Enter fullscreen mode

Exit fullscreen mode

This works for the errors that might crop up as a result of calling fs::read_to_string since this function is capable of returning multiple types of errors. Therefore, we need a way to generically represent those different possible error types; the commonality between them all is the fact that they all implement the Error trait!

Ultimately, what we want to do is define all of these different types of errors in a central location and have them all be variants of a single type.

Defining error variants in a central type

We’ll create a new src/error.rs file and define an enum AppError, deriving the Debug trait in the process so that we can get a debug representation should we need it. We’ll name each of the variants of this enum in such a way that they appropriately represent each of the three types of errors:

#[derive(Debug)]
pub enum AppError {
    MissingQuery,
    MissingFilename,
    ConfigLoad,
}

Enter fullscreen mode

Exit fullscreen mode

The third variant, ConfigLoad, maps to the error that might crop up when calling fs::read_to_string in the Config::run function. This might seem a bit out of place at first, since if an error occurs with that function, it would be some sort of I/O problem reading the provided config file. So why didn’t we name it IOError or something like that?

In this case, since we’re surfacing an error from a standard library function, it’s more relevant to our application to describe how the surfaced error affects it, instead of simply reiterating it. When an error occurs with fs::read_to_string, that prevents our Config from loading, so that’s why we named it ConfigLoad.

Now that we have this type, we need to update all of the spots in our code where we return errors to utilize this AppError enum.

Returning variants of our AppError

At the top of our src/lib.rs file, we need to declare our error module and bring error::AppError into scope:

mod error;

use error::AppError;

Enter fullscreen mode

Exit fullscreen mode

In our Config::new function, we need to update the spots where we were returning static string slices as errors, as well as the return type of the function itself:

- pub fn new(mut args: env::Args) -> Result<Config, &'static str>
+ pub fn new(mut args: env::Args) -> Result<Config, AppError>
    // --snip--

    let query = match args.next() {
        Some(arg) => arg,
-       None => return Err("Didn't get a query string"),
+       None => return Err(AppError::MissingQuery),
    };

    let filename = match args.next() {
        Some(arg) => arg,
-       None => return Err("Didn't get a file name"),
+       None => return Err(AppError::MissingFilename),
    };

    // --snip--

Enter fullscreen mode

Exit fullscreen mode

The third error in the run function only requires us to update its return type, since the ? operator is already taking care of bubbling the error up and returning it should it occur.

- pub fn run(config: Config) -> Result<(), Box<dyn Error>>
+ pub fn run(config: Config) -> Result<(), AppError>

Enter fullscreen mode

Exit fullscreen mode

Ok, so we’re now making use of our error variants, which, should they occur, are being surfaced to our main function and printed out. But we no longer have the actual error messages that we had before defined anywhere!

Annotating error variants with thiserror

The thiserror crate is one that is commonly used to provide an ergonomic way to format error messages in a Rust library.

It allows us to annotate each of the variants in our AppError enum with the actual error message that we want displayed to the end user.

Let’s add it as a dependency in our Cargo.toml:

[dependencies]
thiserror = "1"

Enter fullscreen mode

Exit fullscreen mode

In src/error.rs we’ll bring the thiserror::Error trait into scope and have our AppError type derive it. We need this trait derived in order to annotate each enum variant with an #[error] block. Now we specify the error message that we want displayed for each particular variant:

+ use std::io;

+ use thiserror::Error;

- #[derive(Debug)]
+ #[derive(Debug, Error)]
pub enum AppError {
+   #[error("Didn't get a query string")]
    MissingQuery,
+   #[error("Didn't get a file name")]
    MissingFilename,
+   #[error("Could not load config")]
    ConfigLoad {
+       #[from] 
+       source: io::Error,
+   }
}

Enter fullscreen mode

Exit fullscreen mode

What’s all the extra stuff was added to the ConfigLoad variant? Since a ConfigLoad error only occurs when there’s an underlying error with the call to fs::read_to_string, what the ConfigLoad variant is actually doing is providing extra context around the underlying I/O error.

thiserror allows us to wrap a lower-level error in additional context by annotating it with a #[from] in order to convert the source into our homebrew error type. In this way, when an I/O error occurs (like when we specify a file to search through that doesn’t actually exist), we get an error like this:

Could not load config: Os { code: 2, kind: NotFound, message: "No such file or directory" }

Enter fullscreen mode

Exit fullscreen mode

Without it, the resulting error message looks like this:

Os { code: 2, kind: NotFound, message: "No such file or directory" }

Enter fullscreen mode

Exit fullscreen mode

To a consumer of our library, it’s harder to figure out the source of this error; the additional context helps a lot.

You can find the version of minigrep that uses thiserror here.

A more manual approach

Now we’ll switch gears and look out how we might achieve the same results that thiserror provides us, but without bringing it in as a dependency.

Under the hood, thiserror performs some magic with procedural macros, which can have a noticeable effect on compilation speeds. In the case of minigrep, we have very few error variants and the project is so small that a dependency on thiserror really won’t introduce much of an increase in compilation time, but it could be a consideration in a much larger and more complex project.

So on that note, we’ll wrap up this post by ripping it out and replacing it with our own hand-rolled implementation. The nice thing about going down this route is that we’ll only need to make changes to the src/error.rs file to implement all of the necessary changes (besides, of course, removing thiserror from our Cargo.toml).

[dependencies]
- thiserror = "1"

Enter fullscreen mode

Exit fullscreen mode

Let’s remove all of the annotations that thiserror was providing us. We’ll also replace the thiserror::Error trait with the std::error::Error trait:

- use thiserror::Error;
+ use std::error::Error;

- #[derive(Debug, Error)]
+ #[derive(Error)]
pub enum AppError {
-   #[error("Didn't get a query string")]
    MissingQuery,
-   #[error("Didn't get a file name")]
    MissingFilename,
-   #[error("Could not load config")]
    ConfigLoad {
-      #[from]
       source: io::Error,
    }
}

Enter fullscreen mode

Exit fullscreen mode

In order to get back all of the functionality we just wiped, we’ll need to do three things:

  1. Implement the Display trait for AppError so that our error variants can be displayed to the user.
  2. Implement the Error trait for AppError. This trait represents the basic expectations of an error type, namely that they implement Display and Debug, plus the capability to fetch the underlying source or cause of the error.
  3. Implement From<io::Error> for AppError. This is required so that we can convert an I/O error returned from fs::read_to_string into an instance of AppError.

Here’s our implementation of the Display trait for our AppError. It maps each error variant to an string and writes it to the Display formatter.

use std::fmt;

impl fmt::Display for AppError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::MissingQuery => f.write_str("Didn't get a query string"),
            Self::MissingFilename => f.write_str("Didn't get a file name"),
            Self::ConfigLoad { source } => write!(f, "Could not load config: {}", source),
        }
    }
}

Enter fullscreen mode

Exit fullscreen mode

And here’s our implementation of the Error trait. The main method to be implemented is the Error::source method, which is meant to provide information about the source of an error. For our AppError type, only ConfigLoad exposes any underlying source information, namely the I/O error that might happen as a result of calling fs::read_to_string. There’s no underlying source information to expose in the case of the other error variants.

use std::error;

impl error::Error for AppError {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        match self {
            Self::ConfigLoad { source } => Some(source),
            _ => None,
        }
    }
}

Enter fullscreen mode

Exit fullscreen mode

The &(dyn Error + 'static) part of the return type is similar to the Box<dyn Error> trait object that we saw earlier. The main difference here is that the trait object is behind an immutable reference instead of a Box pointer. The 'static lifetime here means the trait object itself only contains owned values, i.e., it doesn’t store any references internally. This is necessary in order to assuage the compiler that there’s no chance of a dangling pointer here.

Lastly, we need a way to convert an io::Error into an AppError. We’ll do this by impling From<io::error> for AppError.

impl From<io::Error> for AppError {
    fn from(source: io::Error) -> Self {
        Self::ConfigLoad { source }
    }
}

Enter fullscreen mode

Exit fullscreen mode

There’s not much to this one. If we get an io::Error, all we do to convert it to an AppError is wrap it in a ConfigLoad variant.

And that’s all folks! You can find this version of our minigrep implementation here.

Summary

In closing, we discussed how the original minigrep implementation presented in The Rust Programming Language book is a bit lacking in the error handling department, as well as how to think about different error handling use cases.

From there, we showcased how to use the thiserror crate to centralize all of the possible error variants into a single type.

Finally, we peeled back the veneer that thiserror provides and showed how to replicate the same functionality manually.

I hope you all learned something from this post! 🙂

Struct async_session::Error[][src]

#[repr(transparent)]

pub struct Error { /* fields omitted */ }

Expand description

The Error type, a wrapper around a dynamic error type.

Error works a lot like Box<dyn std::error::Error>, but with these
differences:

  • Error requires that the error is Send, Sync, and 'static.
  • Error guarantees that a backtrace is available, even if the underlying
    error type does not provide one.
  • Error is represented as a narrow pointer — exactly one word in
    size instead of two.

When you print an error object using “{}” or to_string(), only the outermost
underlying error or context is printed, not any of the lower level causes.
This is exactly as if you had called the Display impl of the error from
which you constructed your anyhow::Error.

Failed to read instrs from ./path/to/instrs.json

To print causes as well using anyhow’s default formatting of causes, use the
alternate selector “{:#}”.

Failed to read instrs from ./path/to/instrs.json: No such file or directory (os error 2)

The Debug format “{:?}” includes your backtrace if one was captured. Note
that this is the representation you get by default if you return an error
from fn main instead of printing it explicitly yourself.

Error: Failed to read instrs from ./path/to/instrs.json

Caused by:
    No such file or directory (os error 2)

and if there is a backtrace available:

Error: Failed to read instrs from ./path/to/instrs.json

Caused by:
    No such file or directory (os error 2)

Stack backtrace:
   0: <E as anyhow::context::ext::StdError>::ext_context
             at /git/anyhow/src/backtrace.rs:26
   1: core::result::Result<T,E>::map_err
             at /git/rustc/src/libcore/result.rs:596
   2: anyhow::context::<impl anyhow::Context<T,E> for core::result::Result<T,E>>::with_context
             at /git/anyhow/src/context.rs:58
   3: testing::main
             at src/main.rs:5
   4: std::rt::lang_start
             at /git/rustc/src/libstd/rt.rs:61
   5: main
   6: __libc_start_main
   7: _start

To see a conventional struct-style Debug representation, use “{:#?}”.

Error {
    context: "Failed to read instrs from ./path/to/instrs.json",
    source: Os {
        code: 2,
        kind: NotFound,
        message: "No such file or directory",
    },
}

If none of the built-in representations are appropriate and you would prefer
to render the error and its cause chain yourself, it can be done something
like this:

use anyhow::{Context, Result};

fn main() {
    if let Err(err) = try_main() {
        eprintln!("ERROR: {}", err);
        err.chain().skip(1).for_each(|cause| eprintln!("because: {}", cause));
        std::process::exit(1);
    }
}

fn try_main() -> Result<()> {
    ...
}

impl Error[src]

pub fn new<E>(error: E) -> Error where
    E: Error + Send + Sync + 'static, 
[src]

Create a new error object from any error type.

The error type must be threadsafe and 'static, so that the Error
will be as well.

If the error type does not provide a backtrace, a backtrace will be
created here to ensure that a backtrace exists.

pub fn msg<M>(message: M) -> Error where
    M: Display + Debug + Send + Sync + 'static, 
[src]

Create a new error object from a printable error message.

If the argument implements std::error::Error, prefer Error::new
instead which preserves the underlying error’s cause chain and
backtrace. If the argument may or may not implement std::error::Error
now or in the future, use anyhow!(err) which handles either way
correctly.

Error::msg("...") is equivalent to anyhow!("...") but occasionally
convenient in places where a function is preferable over a macro, such
as iterator or stream combinators:

use anyhow::{Error, Result};
use futures::stream::{Stream, StreamExt, TryStreamExt};

async fn demo<S>(stream: S) -> Result<Vec<Output>>
where
    S: Stream<Item = Input>,
{
    stream
        .then(ffi::do_some_work) 
        .map_err(Error::msg)
        .try_collect()
        .await
}

pub fn context<C>(self, context: C) -> Error where
    C: Display + Send + Sync + 'static, 
[src]

Wrap the error value with additional context.

For attaching context to a Result as it is propagated, the
Context extension trait may be more convenient than
this function.

The primary reason to use error.context(...) instead of
result.context(...) via the Context trait would be if the context
needs to depend on some data held by the underlying error:

use anyhow::Result;
use std::fs::File;
use std::path::Path;

struct ParseError {
    line: usize,
    column: usize,
}

fn parse_impl(file: File) -> Result<T, ParseError> {
    ...
}

pub fn parse(path: impl AsRef<Path>) -> Result<T> {
    let file = File::open(&path)?;
    parse_impl(file).map_err(|error| {
        let context = format!(
            "only the first {} lines of {} are valid",
            error.line, path.as_ref().display(),
        );
        anyhow::Error::new(error).context(context)
    })
}

pub fn backtrace(&self) -> &Backtrace[src]

Get the backtrace for this Error.

In order for the backtrace to be meaningful, one of the two environment
variables RUST_LIB_BACKTRACE=1 or RUST_BACKTRACE=1 must be defined
and RUST_LIB_BACKTRACE must not be 0. Backtraces are somewhat
expensive to capture in Rust, so we don’t necessarily want to be
capturing them all over the place all the time.

  • If you want panics and errors to both have backtraces, set
    RUST_BACKTRACE=1;
  • If you want only errors to have backtraces, set
    RUST_LIB_BACKTRACE=1;
  • If you want only panics to have backtraces, set RUST_BACKTRACE=1 and
    RUST_LIB_BACKTRACE=0.

Standard library backtraces are only available on the nightly channel.
Tracking issue: rust-lang/rust#53487.

On stable compilers, this function is only available if the crate’s
“backtrace” feature is enabled, and will use the backtrace crate as
the underlying backtrace implementation.

[dependencies]
anyhow = { version = "1.0", features = ["backtrace"] }

pub fn chain(&self) -> Chain<'_>[src]

An iterator of the chain of source errors contained by this Error.

This iterator will visit every error in the cause chain of this error
object, beginning with the error that this error object was created
from.

use anyhow::Error;
use std::io;

pub fn underlying_io_error_kind(error: &Error) -> Option<io::ErrorKind> {
    for cause in error.chain() {
        if let Some(io_error) = cause.downcast_ref::<io::Error>() {
            return Some(io_error.kind());
        }
    }
    None
}

pub fn root_cause(&self) -> &(dyn Error + 'static)[src]

The lowest level cause of this error — this error’s cause’s
cause’s cause etc.

The root cause is the last error in the iterator produced by
chain().

pub fn is<E>(&self) -> bool where
    E: Display + Debug + Send + Sync + 'static, 
[src]

Returns true if E is the type held by this error object.

For errors with context, this method returns true if E matches the
type of the context C or the type of the error on which the
context has been attached. For details about the interaction between
context and downcasting, see here.

pub fn downcast<E>(self) -> Result<E, Error> where
    E: Display + Debug + Send + Sync + 'static, 
[src]

Attempt to downcast the error object to a concrete type.

pub fn downcast_ref<E>(&self) -> Option<&E> where
    E: Display + Debug + Send + Sync + 'static, 
[src]

Downcast this error object by reference.


match root_cause.downcast_ref::<DataStoreError>() {
    Some(DataStoreError::Censored(_)) => Ok(Poll::Ready(REDACTED_CONTENT)),
    None => Err(error),
}

pub fn downcast_mut<E>(&mut self) -> Option<&mut E> where
    E: Display + Debug + Send + Sync + 'static, 
[src]

Downcast this error object by mutable reference.

pub fn is<T>(&self) -> bool where
    T: 'static + Error, 
1.3.0[src]

Returns true if the boxed type is the same as T

pub fn downcast_ref<T>(&self) -> Option<&T> where
    T: 'static + Error, 
1.3.0[src]

Returns some reference to the boxed value if it is of type T, or
None if it isn’t.

pub fn downcast_mut<T>(&mut self) -> Option<&mut T> where
    T: 'static + Error, 
1.3.0[src]

Returns some mutable reference to the boxed value if it is of type T, or
None if it isn’t.

pub fn is<T>(&self) -> bool where
    T: 'static + Error, 
1.3.0[src]

Forwards to the method defined on the type dyn Error.

pub fn downcast_ref<T>(&self) -> Option<&T> where
    T: 'static + Error, 
1.3.0[src]

Forwards to the method defined on the type dyn Error.

pub fn downcast_mut<T>(&mut self) -> Option<&mut T> where
    T: 'static + Error, 
1.3.0[src]

Forwards to the method defined on the type dyn Error.

pub fn is<T>(&self) -> bool where
    T: 'static + Error, 
1.3.0[src]

Forwards to the method defined on the type dyn Error.

pub fn downcast_ref<T>(&self) -> Option<&T> where
    T: 'static + Error, 
1.3.0[src]

Forwards to the method defined on the type dyn Error.

pub fn downcast_mut<T>(&mut self) -> Option<&mut T> where
    T: 'static + Error, 
1.3.0[src]

Forwards to the method defined on the type dyn Error.

pub fn chain(&self) -> Chain<'_>[src]

🔬 This is a nightly-only experimental API. (error_iter)

Returns an iterator starting with the current error and continuing with
recursively calling Error::source.

If you want to omit the current error and only use its sources,
use skip(1).

#![feature(error_iter)]
use std::error::Error;
use std::fmt;

#[derive(Debug)]
struct A;

#[derive(Debug)]
struct B(Option<Box<dyn Error + 'static>>);

impl fmt::Display for A {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "A")
    }
}

impl fmt::Display for B {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "B")
    }
}

impl Error for A {}

impl Error for B {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        self.0.as_ref().map(|e| e.as_ref())
    }
}

let b = B(Some(Box::new(A)));


let err = &b as &(dyn Error);

let mut iter = err.chain();

assert_eq!("B".to_string(), iter.next().unwrap().to_string());
assert_eq!("A".to_string(), iter.next().unwrap().to_string());
assert!(iter.next().is_none());
assert!(iter.next().is_none());

impl !RefUnwindSafe for Error

impl Send for Error

impl Sync for Error

impl Unpin for Error

impl !UnwindSafe for Error

impl<T> Any for T where
    T: 'static + ?Sized, 
[src]

impl<T> Borrow<T> for T where
    T: ?Sized, 
[src]

impl<T> BorrowMut<T> for T where
    T: ?Sized, 
[src]

impl<T> From<!> for T[src]

pub fn from(t: !) -> T[src]

Performs the conversion.

impl<T> From<T> for T[src]

pub fn from(t: T) -> T[src]

Performs the conversion.

impl<T, U> Into<U> for T where
    U: From<T>, 
[src]

pub fn into(self) -> U[src]

Performs the conversion.

impl<T> Same<T> for T

type Output = T

Should always be Self

impl<T> ToString for T where
    T: Display + ?Sized, 
[src]

impl<T, U> TryFrom<U> for T where
    U: Into<T>, 
[src]

impl<T, U> TryInto<U> for T where
    U: TryFrom<T>, 
[src]

impl<V, T> VZip<V> for T where
    V: MultiLane<T>, 

pub fn vzip(self) -> V

Command Line Applications in Rust

Nicer error reporting

We all can do nothing but accept the fact that errors will occur.
And in contrast to many other languages,
it’s very hard not to notice and deal with this reality
when using Rust:
As it doesn’t have exceptions,
all possible error states are often encoded in the return types of functions.

Results

A function like read_to_string doesn’t return a string.
Instead, it returns a Result
that contains either
a String
or an error of some type
(in this case std::io::Error).

How do you know which it is?
Since Result is an enum,
you can use match to check which variant it is:


#![allow(unused)]
fn main() {
let result = std::fs::read_to_string("test.txt");
match result {
    Ok(content) => { println!("File content: {}", content); }
    Err(error) => { println!("Oh noes: {}", error); }
}
}

Unwrapping

Now, we were able to access the content of the file,
but we can’t really do anything with it after the match block.
For this, we’ll need to somehow deal with the error case.
The challenge is that all arms of a match block need to return something of the same type.
But there’s a neat trick to get around that:


#![allow(unused)]
fn main() {
let result = std::fs::read_to_string("test.txt");
let content = match result {
    Ok(content) => { content },
    Err(error) => { panic!("Can't deal with {}, just exit here", error); }
};
println!("file content: {}", content);
}

We can use the String in content after the match block.
If result were an error, the String wouldn’t exist.
But since the program would exit before it ever reached a point where we use content,
it’s fine.

This may seem drastic,
but it’s very convenient.
If your program needs to read that file and can’t do anything if the file doesn’t exist,
exiting is a valid strategy.
There’s even a shortcut method on Results, called unwrap:


#![allow(unused)]
fn main() {
let content = std::fs::read_to_string("test.txt").unwrap();
}

No need to panic

Of course, aborting the program is not the only way to deal with errors.
Instead of the panic!, we can also easily write return:

fn main() -> Result<(), Box<dyn std::error::Error>> {
let result = std::fs::read_to_string("test.txt");
let content = match result {
    Ok(content) => { content },
    Err(error) => { return Err(error.into()); }
};
Ok(())
}

This, however changes the return type our function needs.
Indeed, there was something hidden in our examples all this time:
The function signature this code lives in.
And in this last example with return,
it becomes important.
Here’s the full example:

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let result = std::fs::read_to_string("test.txt");
    let content = match result {
        Ok(content) => { content },
        Err(error) => { return Err(error.into()); }
    };
    println!("file content: {}", content);
    Ok(())
}

Our return type is a Result!
This is why we can write return Err(error); in the second match arm.
See how there is an Ok(()) at the bottom?
It’s the default return value of the function and means
“Result is okay, and has no content”.

Question Mark

Just like calling .unwrap() is a shortcut
for the match with panic! in the error arm,
we have another shortcut for the match that returns in the error arm:
?.

That’s right, a question mark.
You can append this operator to a value of type Result,
and Rust will internally expand this to something very similar to
the match we just wrote.

Give it a try:

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let content = std::fs::read_to_string("test.txt")?;
    println!("file content: {}", content);
    Ok(())
}

Very concise!

Providing Context

The errors you get when using ? in your main function are okay,
but they are not great.
For example:
When you run std::fs::read_to_string("test.txt")?
but the file test.txt doesn’t exist,
you get this output:

Error: Os { code: 2, kind: NotFound, message: "No such file or directory" }

In cases where your code doesn’t literally contain the file name,
it would be very hard to tell which file was NotFound.
There are multiple ways to deal with this.

For example, we can create our own error type,
and then use that to build a custom error message:

#[derive(Debug)]
struct CustomError(String);

fn main() -> Result<(), CustomError> {
    let path = "test.txt";
    let content = std::fs::read_to_string(path)
        .map_err(|err| CustomError(format!("Error reading `{}`: {}", path, err)))?;
    println!("file content: {}", content);
    Ok(())
}

Now,
running this we’ll get our custom error message:

Error: CustomError("Error reading `test.txt`: No such file or directory (os error 2)")

Not very pretty,
but we can easily adapt the debug output for our type later on.

This pattern is in fact very common.
It has one problem, though:
We don’t store the original error,
only its string representation.
The often used anyhow library has a neat solution for that:
similar to our CustomError type,
its Context trait can be used to add a description.
Additionally, it also keeps the original error,
so we get a “chain” of error messages pointing out the root cause.

Let’s first import the anyhow crate by adding
anyhow = "1.0" to the [dependencies] section
of our Cargo.toml file.

The full example will then look like this:

use anyhow::{Context, Result};

fn main() -> Result<()> {
    let path = "test.txt";
    let content = std::fs::read_to_string(path)
        .with_context(|| format!("could not read file `{}`", path))?;
    println!("file content: {}", content);
    Ok(())
}

This will print an error:

Error: could not read file `test.txt`

Caused by:
    No such file or directory (os error 2)

Error handling in Rust is very different if you’re coming from other languages. In languages like Java, JS, Python etc, you usually throw exceptions and return successful values. In Rust, you return something called a Result.

The Result<T, E> type is an enum that has two variants — Ok(T) for successful value or Err(E) for error value:

enum Result<T, E> {
   Ok(T),
   Err(E),
}

Returning errors instead of throwing them is a paradigm shift in error handling. If you’re new to Rust, there will be some friction initially as it requires you to reason about errors in a different way.

In this post, I’ll go through some common error handling patterns so you gradually become familiar with how things are done in Rust:

  • Ignore the error
  • Terminate the program
  • Use a fallback value
  • Bubble up the error
  • Bubble up multiple errors
  • Match boxed errors
  • Libraries vs Applications
  • Create custom errors
  • Bubble up custom errors
  • Match custom errors

Ignore the error

Let’s start with the simplest scenario where we just ignore the error. This sounds careless but has a couple of legitimate use cases:

  • We’re prototyping our code and don’t want to spend time on error handling.
  • We’re confident that the error won’t occur.

Let’s say that we’re reading a file which we’re pretty sure would be present:

use std::fs;

fn main() {
  let content = fs::read_to_string("./Cargo.toml").unwrap();
  println!("{}", content)
}

Even though we know that the file would be present, the compiler has no way of knowing that. So we use unwrap to tell the compiler to trust us and return the value inside. If the read_to_string function returns an Ok() value, unwrap will get the contents of Ok and assign it to the content variable. If it returns an error, it will “panic”. Panic either terminates the program or exits the current thread.

Note that unwrap is used in quite a lot of Rust examples to skip error handling. This is mostly done for convenience and shouldn’t be used in real code as it is.

Terminate the program

Some errors cannot be handled or recovered from. In these cases, it’s better to fail fast by terminating the program.

Let’s use the same example as above — we’re reading a file which we’re sure to be present. Let’s imagine that, for this program, that file is absolutely important without which it won’t work properly. If for some reason, this file is absent, it’s better to terminate the program.

We can use unwrap as before or use expect — it’s same as unwrap but lets us add extra error message.

use std::fs;

fn main() {
  let content = fs::read_to_string("./Cargo.toml").expect("Can't read Cargo.toml");
  println!("{}", content)
}

See also: panic!

Use a fallback value

In some cases, you can handle the error by falling back to a default value.

For example, let’s say we’re writing a server and the port it listens to can be configured using an environment variable. If the environment variable is not set, accessing that value would result in an error. But we can easily handle that by falling back to a default value.

use std::env;

fn main() {
  let port = env::var("PORT").unwrap_or("3000".to_string());
  println!("{}", port);
}

Here, we’ve used a variation of unwrap called unwrap_or which lets us supply default values.

See also: unwrap_or_else, unwrap_or_default

Bubble up the error

When you don’t have enough context to handle the error, you can bubble up (propagate) the error to the caller function.

Here’s a contrived example which uses a webservice to get the current year:

use std::collections::HashMap;

fn main() {
  match get_current_date() {
    Ok(date) => println!("We've time travelled to {}!!", date),
    Err(e) => eprintln!("Oh noes, we don't know which era we're in! :( n  {}", e),
  }
}

fn get_current_date() -> Result<String, reqwest::Error> {
  let url = "https://postman-echo.com/time/object";
  let result = reqwest::blocking::get(url);

  let response = match result {
    Ok(res) => res,
    Err(err) => return Err(err),
  };

  let body = response.json::<HashMap<String, i32>>();

  let json = match body {
    Ok(json) => json,
    Err(err) => return Err(err),
  };

  let date = json["years"].to_string();

  Ok(date)
}

There are two function calls inside the get_current_date function (get and json) that return Result values. Since get_current_date doesn’t have context of what to do when they return errors, it uses pattern matching to propagate the errors to main.

Using pattern matching to handle multiple or nested errors can make your code “noisy”. Instead, we can rewrite the above code using the ? operator:

use std::collections::HashMap;

fn main() {
  match get_current_date() {
    Ok(date) => println!("We've time travelled to {}!!", date),
    Err(e) => eprintln!("Oh noes, we don't know which era we're in! :( n  {}", e),
  }
}

fn get_current_date() -> Result<String, reqwest::Error> {
  let url = "https://postman-echo.com/time/object";
  let res = reqwest::blocking::get(url)?.json::<HashMap<String, i32>>()?;
  let date = res["years"].to_string();

  Ok(date)
}

This looks much cleaner!

The ? operator is similar to unwrap but instead of panicking, it propagates the error to the calling function. One thing to keep in mind is that we can use the ? operator only for functions that return a Result or Option type.

Bubble up multiple errors

In the previous example, the get and json functions return a reqwest::Error error which we’ve propagated using the ? operator. But what if we’ve another function call that returned a different error value?

Let’s extend the previous example by returning a formatted date instead of the year:

+ use chrono::NaiveDate;
  use std::collections::HashMap;

  fn main() {
    match get_current_date() {
      Ok(date) => println!("We've time travelled to {}!!", date),
      Err(e) => eprintln!("Oh noes, we don't know which era we're in! :( n  {}", e),
    }
  }

  fn get_current_date() -> Result<String, reqwest::Error> {
    let url = "https://postman-echo.com/time/object";
    let res = reqwest::blocking::get(url)?.json::<HashMap<String, i32>>()?;
-   let date = res["years"].to_string();
+   let formatted_date = format!("{}-{}-{}", res["years"], res["months"] + 1, res["date"]);
+   let parsed_date = NaiveDate::parse_from_str(formatted_date.as_str(), "%Y-%m-%d")?;
+   let date = parsed_date.format("%Y %B %d").to_string();

    Ok(date)
  }

The above code won’t compile as parse_from_str returns a chrono::format::ParseError error and not reqwest::Error.

We can fix this by Boxing the errors:

  use chrono::NaiveDate;
  use std::collections::HashMap;

  fn main() {
    match get_current_date() {
      Ok(date) => println!("We've time travelled to {}!!", date),
      Err(e) => eprintln!("Oh noes, we don't know which era we're in! :( n  {}", e),
    }
  }

- fn get_current_date() -> Result<String, reqwest::Error> {
+ fn get_current_date() -> Result<String, Box<dyn std::error::Error>> {
    let url = "https://postman-echo.com/time/object";
    let res = reqwest::blocking::get(url)?.json::<HashMap<String, i32>>()?;

    let formatted_date = format!("{}-{}-{}", res["years"], res["months"] + 1, res["date"]);
    let parsed_date = NaiveDate::parse_from_str(formatted_date.as_str(), "%Y-%m-%d")?;
    let date = parsed_date.format("%Y %B %d").to_string();

    Ok(date)
  }

Returning a trait object Box<dyn std::error::Error> is very convenient when we want to return multiple errors!

See also: anyhow, eyre

Match boxed errors

So far, we’ve only printed the errors in the main function but not handled them. If we want to handle and recover from boxed errors, we need to “downcast” them:

  use chrono::NaiveDate;
  use std::collections::HashMap;

  fn main() {
    match get_current_date() {
      Ok(date) => println!("We've time travelled to {}!!", date),
-     Err(e) => eprintln!("Oh noes, we don't know which era we're in! :( n  {}", e),
+     Err(e) => {
+       eprintln!("Oh noes, we don't know which era we're in! :(");
+       if let Some(err) = e.downcast_ref::<reqwest::Error>() {
+         eprintln!("Request Error: {}", err)
+       } else if let Some(err) = e.downcast_ref::<chrono::format::ParseError>() {
+         eprintln!("Parse Error: {}", err)
+       }
+     }
    }
  }

  fn get_current_date() -> Result<String, Box<dyn std::error::Error>> {
    let url = "https://postman-echo.com/time/object";
    let res = reqwest::blocking::get(url)?.json::<HashMap<String, i32>>()?;

    let formatted_date = format!("{}-{}-{}", res["years"], res["months"] + 1, res["date"]);
    let parsed_date = NaiveDate::parse_from_str(formatted_date.as_str(), "%Y-%m-%d")?;
    let date = parsed_date.format("%Y %B %d").to_string();

    Ok(date)
  }

Notice how we need to be aware of the implementation details (different errors inside) of get_current_date to be able to downcast them inside main.

See also: downcast, downcast_mut

Applications vs Libraries

As mentioned previously, the downside to boxed errors is that if we want to handle the underlying errors, we need to be aware of the implementation details. When we return something as Box<dyn std::error::Error>, the concrete type information is erased. To handle the different errors in different ways, we need to downcast them to concrete types and this casting can fail at runtime.

However, saying something is a “downside” is not very useful without context. A good rule of thumb is to question whether the code you’re writing is an “application” or a “library”:

Application

  • The code you’re writing would be used by end users.
  • Most errors generated by application code won’t be handled but instead logged or reported to the user.
  • It’s okay to use boxed errors.

Library

  • The code you’re writing would be consumed by other code. A “library” can be open source crates, internal libraries etc
  • Errors are part of your library’s API, so your consumers know what errors to expect and recover from.
  • Errors from your library are often handled by your consumers so they need to be structured and easy to perform exhaustive match on.
  • If you return boxed errors, then your consumers need to be aware of the errors created by your code, your dependencies, and so on!
  • Instead of boxed errors, we can return custom errors.

Create custom errors

For library code, we can convert all the errors to our own custom error and propagate them instead of boxed errors. In our example, we currently have two errors — reqwest::Error and chrono::format::ParseError. We can convert them to MyCustomError::HttpError and MyCustomError::ParseError respectively.

Let’s start by creating an enum to hold our two error variants:

// error.rs

pub enum MyCustomError {
  HttpError,
  ParseError,
}

The Error trait requires us to implement the Debug and Display traits:

// error.rs

use std::fmt;

#[derive(Debug)]
pub enum MyCustomError {
  HttpError,
  ParseError,
}

impl std::error::Error for MyCustomError {}

impl fmt::Display for MyCustomError {
  fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
    match self {
      MyCustomError::HttpError => write!(f, "HTTP Error"),
      MyCustomError::ParseError => write!(f, "Parse Error"),
    }
  }
}

We’ve created our own custom error!

This is obviously a simple example as the error variants don’t contain much information about the error. But this should be sufficient as a starting point for creating more complex and realistic custom errors. Here are some real life examples: ripgrep, reqwest, csv and serde_json

See also: thiserror, snafu

Bubble up custom errors

Let’s update our code to return the custom errors we just created:

  // main.rs

+ mod error;

  use chrono::NaiveDate;
+ use error::MyCustomError;
  use std::collections::HashMap;

  fn main() {
    // skipped, will get back later
  }

- fn get_current_date() -> Result<String, Box<dyn std::error::Error>> {
+ fn get_current_date() -> Result<String, MyCustomError> {
    let url = "https://postman-echo.com/time/object";
-   let res = reqwest::blocking::get(url)?.json::<HashMap<String, i32>>()?;
+   let res = reqwest::blocking::get(url)
+     .map_err(|_| MyCustomError::HttpError)?
+     .json::<HashMap<String, i32>>()
+     .map_err(|_| MyCustomError::HttpError)?;

    let formatted_date = format!("{}-{}-{}", res["years"], res["months"] + 1, res["date"]);
-   let parsed_date = NaiveDate::parse_from_str(formatted_date.as_str(), "%Y-%m-%d")?;
+   let parsed_date = NaiveDate::parse_from_str(formatted_date.as_str(), "%Y-%m-%d")
+     .map_err(|_| MyCustomError::ParseError)?;
    let date = parsed_date.format("%Y %B %d").to_string();

    Ok(date)
  }

Notice how we’re using map_err to convert the error from one type to another type.

But things got verbose as a result — our function is littered with these map_err calls. We can implement the From trait to automatically coerce the error types when we use the ? operator:

  // error.rs

  use std::fmt;

  #[derive(Debug)]
  pub enum MyCustomError {
    HttpError,
    ParseError,
  }

  impl std::error::Error for MyCustomError {}

  impl fmt::Display for MyCustomError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
      match self {
        MyCustomError::HttpError => write!(f, "HTTP Error"),
        MyCustomError::ParseError => write!(f, "Parse Error"),
      }
    }
  }

+ impl From<reqwest::Error> for MyCustomError {
+   fn from(_: reqwest::Error) -> Self {
+     MyCustomError::HttpError
+   }
+ }

+ impl From<chrono::format::ParseError> for MyCustomError {
+   fn from(_: chrono::format::ParseError) -> Self {
+     MyCustomError::ParseError
+   }
+ }
  // main.rs

  mod error;

  use chrono::NaiveDate;
  use error::MyCustomError;
  use std::collections::HashMap;

  fn main() {
    // skipped, will get back later
  }

  fn get_current_date() -> Result<String, MyCustomError> {
    let url = "https://postman-echo.com/time/object";
-   let res = reqwest::blocking::get(url)
-     .map_err(|_| MyCustomError::HttpError)?
-     .json::<HashMap<String, i32>>()
-     .map_err(|_| MyCustomError::HttpError)?;
+   let res = reqwest::blocking::get(url)?.json::<HashMap<String, i32>>()?;

    let formatted_date = format!("{}-{}-{}", res["years"], res["months"] + 1, res["date"]);
-   let parsed_date = NaiveDate::parse_from_str(formatted_date.as_str(), "%Y-%m-%d")
-     .map_err(|_| MyCustomError::ParseError)?;
+   let parsed_date = NaiveDate::parse_from_str(formatted_date.as_str(), "%Y-%m-%d")?;
    let date = parsed_date.format("%Y %B %d").to_string();

    Ok(date)
  }

We’ve removed map_err and the code looks much cleaner!

However, From trait is not magic and there are times when we need to use map_err. In the above example, we’ve moved the type conversion from inside the get_current_date function to the From<X> for MyCustomError implementation. This works well when the information needed to convert from one error to MyCustomError can be obtained from the original error object. If not, we need to use map_err inside get_current_date.

Match custom errors

We’ve ignored the changes in main until now, here’s how we can handle the custom errors:

  // main.rs

  mod error;

  use chrono::NaiveDate;
  use error::MyCustomError;
  use std::collections::HashMap;

  fn main() {
    match get_current_date() {
      Ok(date) => println!("We've time travelled to {}!!", date),
      Err(e) => {
        eprintln!("Oh noes, we don't know which era we're in! :(");
-       if let Some(err) = e.downcast_ref::<reqwest::Error>() {
-         eprintln!("Request Error: {}", err)
-       } else if let Some(err) = e.downcast_ref::<chrono::format::ParseError>() {
-         eprintln!("Parse Error: {}", err)
-       }
+       match e {
+         MyCustomError::HttpError => eprintln!("Request Error: {}", e),
+         MyCustomError::ParseError => eprintln!("Parse Error: {}", e),
+       }
      }
    }
  }

  fn get_current_date() -> Result<String, MyCustomError> {
    let url = "https://postman-echo.com/time/object";
    let res = reqwest::blocking::get(url)?.json::<HashMap<String, i32>>()?;

    let formatted_date = format!("{}-{}-{}", res["years"], res["months"] + 1, res["date"]);
    let parsed_date = NaiveDate::parse_from_str(formatted_date.as_str(), "%Y-%m-%d")?;
    let date = parsed_date.format("%Y %B %d").to_string();

    Ok(date)
  }

Notice how unlike boxed errors, we can actually match on the variants inside MyCustomError enum.

Conclusion

Thanks for reading! I hope this post was helpful in introducing the basics of error handling in Rust. I’ve added the examples to a repo in GitHub which you can use for practice. If you’ve more questions, please contact me at sheshbabu [at] gmail.com. Feel free to follow me in Twitter for more posts like this :)

Понравилась статья? Поделить с друзьями:
  • Rust compile error macro
  • Rust cheat checker ошибка вас не вызвали на проверку
  • Rust application error 1000
  • Russian error page
  • Rush royale код ошибки 13