With people reflecting on Rust in 2019 and what they want to see in 2020, error handling has come up again:

It felt like there was interest in moving anyhow into the stdlib. While I feel that a lot of it is ready or near-ready (see below), I feel we have gotten stuck in a rut with the context pattern. I've been told that the pattern is derived from cargo and it feels like we've mostly been iterating on that same pattern rather than exploring alternatives. After framing my concerns with error handling and where we are at with existing solutions, I'll introduce Status as a radical alternative (from the Rust ecosystem perspective) for addressing these problems. If you aren't interested in all the middle stuff, feel free to skip to the end!

Update: I messed up my reading of docs and test cases and claimed that Box<dyn Error> implemented Error. This is not the case and my post has been updated to reflect that.

Why are errors such a hot topic in Rust compared to other languages?

When I look at other languages, for the most part what error is returned is an implementation detail unless stated otherwise in documentation. This can be achieved with Box<dyn Error> but with trade-offs

Other challenges that are unique to Rust:

Challenges that I feel don't just apply to Rust:

Status of solutions

Spoiler alert: I started this post as an introduction for Status but it grew from there. What's worse, is I felt it best to order things from more concrete plans to more wild ideas. If you are interested in a Proof-of-Concept that tries to address (almost) all of the above problems, I recommend skipping to the end.

Concrete error types

thiserror is a fairly mature solution for reducing the boilerplate. However, I feel it should narrow its focus on just trait Error. We should instead have a generalized solution for deriving Display and From.

With those features removed, I'd love to see this moved into the stdlib.

Box<dyn Error>

Some might see anyhow filling this role but I feel we should decouple adhoc error handling from an abstract error container.

There was a discussion about a BoxError on internals and I feel this is the way to go. I feel we should have something that looks like:

#[derive(Debug. anyhow::Error, derive_more::Display, derive_more::From)]
struct BoxError(Box<Box<dync Error + Send + Sync + 'static>>);

This does not solve cloning, serialization, localization, or a host of other issues, however.

For me, the main open questions before putting this in the stdlib are:

Ad-hoc Error Types

I think anyhow::Error and anyhow::anyhow! are close to something we can standardize. My main concerns

From for ?

The first problem is From<dep::Errpr> exposing implementation details.

The second problem is From<E: Error>

Streamlined Syntax

This is a more diverse space

Everything else

Introducing my Proof-of-Concept, Status. I refer to it as an error container because it provides a basic structure for the major parts of an error which you then populate.

Unlike the error-wrapping pattern found in cargo and generalized in anyhow, the pattern implemented in Status comes from projects I've worked on which try to address the following requirements:

These requirements are addressed by trading off the usability provided by per-site custom messages with messages built up from common building blocks. The Kind serves as a static description of the error that comes from a general, fixed collection. Describing the exact problem and tailored remediation is the responsibility of the Context which maps general, fixed keys with runtime-generated data.

Status grows from your prototype to a mature library.

A prototype might look like:

use std::path::Path;
type Status = status::Status;
type Result<T, E = Status> = std::result::Result<T, E>;

fn read_file(path: &Path) -> Result<String> {
    std::fs::read_to_string(path)
        .map_err(|e| {
            Status::new("Failed to read file")
                .with_internal(e)
                .context_with(|c| c.insert("Expected value", 5))
        })
}

fn main() -> Result<(), status::TerminatingStatus> {
    let content = read_file(Path::new("Cargo.toml"))?;
    println!("{}", content);
    Ok(())
}

The TerminatingStatus provides a Debug that produces user-visible data and will print chained error messages, so the output will looks something like:

Failed to read file

Expected value: 5

You can then customize the Kind and Context used.

use std::path::Path;
#[derive(Copy, Clone, Debug, derive_more::Display)]
enum ErrorKind {
  #[display(fmt = "Failed to read file")]
  Read,
  #[display(fmt = "Failed to parse")]
  Parse,
}
type Status = status::Status<ErrorKind>;
type Result<T, E = Status> = std::result::Result<T, E>;

fn read_file(path: &Path) -> Result<String, Status> {
    std::fs::read_to_string(path)
        .map_err(|e| {
            Status::new(ErrorKind::Read).with_internal(e)
                .context_with(|c| c.insert("Expected value", 5))
        })
}

(custom Context not shown)

For more, check out the docs and the issues. Like I said, this is a Proof-of-Concept. Your help is needed, whether feedback, ideas, or code. Even something as simple as encouragement that this is worth pursuing would be good feedback into where I put my time.