The state of error handling in the 2018 edition


#1

After being out of the loop (not following this-week-in-rust as closely as I used to) for several months, I have the opportunity of starting a small/medium binary Rust project at work. I choose Rust for its safety, and the simplicity of delivering a binary with a low footprint.

Before I start typing code, I think I should decide upon a strategy for error handling.

What is the recommended approach for dealing with errors. I want to be as productive (having minimal boiler plate to deal with) and correct as possible, while the resulting size and speed are less of an issue, since I expect that whatever I will get with Rust will be good enough in the performance department.

In particular what crates if any should I look at? Any pointers to a beginner tutorial or using such crates, and or patterns, will also be welcome.


Error Handling port
#2

The “2018 Edition” doesn’t change anything for error handling. Current recommended strategy is the same as before. Ideally, you would use a custom Error + ErrorKind pair based on the failure crate if you want to handle a number of different possible error types and to be able to iterate on causes.

function_that_errors()
    .context(ErrorKind::SpecificError)?;

If you need to include additional context:

function(&path)
    .map_err(|why| {
        why.context(ErrorKind::ThingHappened(path))
    })?;

Otherwise, just use failure-derive to enhance any enum into an error type by automatically deriving Display + Error + Fail on it. Basically, the ErrorKind without the Error machinery.

#[derive(Debug, Fail)]
enum ErrorKind {
    #[fail(display = "unable to do thing at {:?}", _0)]
    ThingHappened(PathBuf),
    #[fail(display = "resource not found")]
    ResourceNotFound
}

#3

The standard Rust error handling works well for me and I have been happy with it.

The one downside I found with Rust error handling is having to convert between error types sometimes, e.g an io::Error becomes a MyApp::Error as it is propagated up through callers. Two crates that help with this type conversion are error-chain and failure. I have used both and I personally prefer failure but either of these crates do a good job and work with the standard error handling and so that you can add them when you need them.

For logging errors I have been happy using the log crate with simplelog.

None of this is an expert opinion, but I found this setup beginner friendly and had no problems after investing some time.


#4

I probably should have said “In the 2018 Edition era”. I didn’t meant that something have changed in the transition to the 2018 edition, but that something is always changing in the ecosystem and that I wanted to use the current patterns.

Thanks. The examples you gave seem straight forward enough, and it is good to know that failure is still considered a good approach.

Also according to failure’s README:

Failure is currently evolving as a library. First of all there is work going on in Rust itself to fix the error trait secondarily the original plan for Failure towards 1.0 is unlikely to happen in the current form.
As such the original master branch towards 1.0 of failure was removed and master now represents the future iteration steps of 0.1 until it’s clear what happens in the stdlib.
The original 1.0 branch can be found in evolution/1.0.

For mere mortals, what should I expect to happen in the near/mid future? Is there any blog post I can read about it?


#5

I don’t know if anyone knows the specific roadmap. Unless failure stays at 0.1 or otherwise finds a way to make a semver incompatible release that makes a slow migration possible, we should expect some amount of pain in that migration roughly proportional to the number of folks that have chosen to adopt failure in their libraries. I think that number is fairly small in core libraries, which is good.

I personally continue to use the approach to error handling I described a few years ago. To be succinct, if you’re building a library, then I am definitely not using failure, if only for the reason that the library still has some growing to do. If I am building an application, then failure is an option if you’re OK with a migration in the future and if you really like what failure gives you, like derive(Fail) and backtraces.

Personally, I don’t think I’d unreservedly say that “failure is the current recommended strategy” without the above qualifications.


#6

I would make one small but significant addition to broaden the focus. Crates like human-panic and a bunch from the CLI WG / ecosystem can really help dealing with the user experience of error handling, as well as the programmer experience.


#7

I’m a fan of quick-error. It really is a quick way to make an enum of a handful of error kinds that a library may need.


#8

While I know the rust way of doing things is to use a Result<Ok, Err> I prefer to use a direct panic when the error is application breaking for ease of debugging, because if the stack when the error is first propagated looks like so:
A -> B -> C -> D -> E -> F -> G -> H -> I
and I only get a panic at B per say, then how should I know that it originated in I and not in B, C, D, E, F, G, H, or I? If I understand it correctly, when passing up a result/error one usually interprets it into a function’s own return result, which may overwrite the original Err type (It probably will), which may lead to something like Error: Cannot read file: "./plants/peach/Instructions.txt becoming Error: Cannot become interested in growing a peach tree and watering it (Which is completely unrelated, and unclear…)


#9

A library for error handling that support kind of “throws” in function signatures: CeX


#10

The following is pretty much echoing @BurntSushi’s point if I understood correctly - so sorry if it’s a bit redundant, but I don’t think you really need more than Option and Result, especially once you introduce your own enums for different kinds of errors.

Chaining them together and converting between them can take some getting used to - but I’ve found it to be really nice once I get it working, to have all the error handling in one place and no panics.

If you haven’t seen it yet, this talk is really worth watching… he’s using F# but the ideas are universal (or rather, they’re specific in Rust via the Option and Result enums):

Searching for “railway oriented programming” shows some other talks too. He’s also an enjoyable speaker and makes the topic super approachable imho.

Take all this with a grain of salt since I’m just like 10 hours into Rust so far and haven’t built anything at large-scale yet… might be that I will need to reach for a library at some point :wink:


#11

Surely you realise “Failure is not an option”. :slight_smile:


#12

I think Failure is, by the common sense, an Error, and so it’s really not an Option, but a Result. We won’t say that Failure == None, will we?


#13

Failure’s #[derive(Fail)] gives you the opportunity to chain the original Error with the #[cause] attribute. Your top-level error handler can then print the entire chain of errors by iterating Fail::iter_causes.

For example, here is my top-level error handler in cargo-n64. You only need one! The ? operator and From impls take care of the rest.

And here’s what a typical error looks like when printed by this handler:

23%20PM

Of course, you should customize it to your liking. Here’s the same error (in release mode) after hacking in a backtrace to my impl From<io::Error> for IPL3Error:

Debug mode builds have better backtraces:

Anyway, you should be able to get all of the debug context you need out of Failure. It’s really crazy super useful for application developers. As mentioned by others though, I might be wary of using it in library crates.


#14

This sounds amazing! I’m usually reluctant to try new crates/libraries (like Failure), because of a fear of me jumping off on a tangent, but this seems pretty useful, even if it takes some time to get used to. And yeah, it doesn’t seem to logical to include it in a library, where the user should take care of library errors, but in a binary, it should be great! With that being said, I don’t do many I/O things, so I don’t see myself using this too often.


#15

io::Error is only one case where I’m doing error handling (and any non-trivial app would handle various errors of all sorts). For example, I have to parse JSON and ELF files; those parsers have their own error variants. I execute an external command (cargo) and it can fail in many ways, … Basically I leverage Result and ? a lot, and fewer than half of the Error types come from std::io.


#16

Failure’s getting kind of old at this point. I’ve been using it for a little more than a year. If it makes you feel any better, we’re using it in all our crates and projects at System76. It simply takes care of every possible error handling strategy without the majority of the boilerplate required to set it up.

There’s no one true way to use the failure crate. You can use it to construct any kind of error handling. The compiler macros are especially useful for automatically deriving Display + Error + Fail for an enum. The Fail trait makes it trivial to set up custom error types and error chaining.

The ability to iterate on causes or fetch a root cause of an error is especially useful. Having just the immediate cause, or just the root cause, typically isn’t enough to diagnose the true nature of an error. ErrorKind contexts are often pretty useful to give the caller greater flexibility on how to handle a specific error, or error combination.


#17

(I really shouldn’t try to make jokes on these pages, should I?)


#18

This is actually the point of panicking! There is only sense in returning a Result when the error reported is meaningful information that the caller can use.

For more on this philosophy, see: http://joeduffyblog.com/2016/02/07/the-error-model/


#20

The best thing for the ergonomics of error handling is the ? (question mark operator).

Until the RFC is released to enable try { ... } blocks you may use the crate ::candy and its catch! macro to already enjoy the nice error handling:

#[macro_use(catch)] extern crate candy;
// or, with the 2018 edition,
// use ::candy::catch;

use std::{
    fs,
    io::{self, Write},
};

const FILENAME: &str = "foo.txt";
const MESSAGE: &str = "Hello, candy!";

fn main ()
{
    let s = catch!(
        // 1st, let's write to some file;
        fs::OpenOptions::
            new()
                .create_new(true) // you may try already having a 'foo.txt' file to see the error in action
                .write(true)
                .open(FILENAME)? // do open
                .write_all(MESSAGE.as_bytes())?; // do write
        // closed here on drop.

        // 2nd, let's read the file and check its output
        let contents = fs::read_to_string(FILENAME)?;
        fs::remove_file(FILENAME)?;

        assert_eq!(contents, MESSAGE); // panic on race condition
        format!("{:?}", contents)
    ).unwrap_or_else(|e: io::Error| {
        format!("an error: {}", e)
    });
    println!("We got {}", s);
}
$ cargo run
  [...]
We got "Hello, candy!"

$ touch foo.txt && cargo run
   [...]
We got an error: File exists (os error 17)