Proposal: add std::error::BoxError

This is a request for comments in the literal sense, also sorry about it being a bit bikesheddy - it's about showcasing a workflow in the api docs, as well as saving me a few keystrokes.

The proposal is to add

// open question: what about `Send`,` Sync`, not `'static`.
type BoxError = Box<dyn std::error::Error + 'static>; 

and in the documentation explain how you can return Result<T, BoxError> and lots of things like std::io::Error will coerce to it. For example

fn lots_of_different_errors() -> Result<(), BoxError> {
    let file_contents = fs::read_file_contents("some/path")?;
    let parsed = serde_json::from_str(&file_contents)?;
    let different_object = parsed.try_into()?;
    // This also may return BoxError
    do_something(different_objects)
}

It shows how you can do run-time polymorphism with arguably its most useful example - Errors.

Pros

  1. Saves some keystrokes
  2. Standardizes the name for Box<Error + 'static + etc..>, and also standardizes which auto traits it implements
  3. Gives a place in the API docs to explain the trait object error pattern
  4. Easy/low risk to implement

Cons

  1. Doesn't add that much and increases surface area
  2. Forces people to use a specific combination of 'static + Send + Sync

Anyway just putting it out there to see what people think.

10 Likes

+1. As DynError (Box is an implementation detail, dyn is the important part).

I suggest with Send + Sync, because it's hard to add them later. Also they're more to type, so it makes a bigger payoff for the alias.

7 Likes

+1, but I suggest making it an opaque type (instead of Box<dyn ...>), and implement using a thin pointer, just like failure does on nightly.

1 Like

Yes yes yes. Not to mention that most of the time it's very nice or even required that an error type be E: Send + Sync, most notably in the case of compiler-synthesized, Result-returning #[test] functions, which run on many threads.

I actually place a type Error = Box<dyn std::error::Error + Send + Sync> at the top of little programs quite often, if I don't need the full bloat of failure. I often now prefer error-derive too.

So I'm in favor (and I prefer the Box one over an opaque type, but it would be nice if few rough edges around dynamic dispatch were improved too ‒ for example, such Error doesn't implement std::error::Error, which bites around generic code).

Anyway, using BoxError as the name kind of uses the name of implementation detail. I'd prefer having it be named after the purpose, so what about AnyError?

Also, I'd strongly prefer having Send + Sync there. Some libraries return Box<dyn Error> and working with that is problematic even though the all wrapped errors are in fact Send and Sync. So I think having errors them Send + Sync should be the default.

5 Likes

(Once we have quantification over bounds we could generalize with:

type AnyError<dyn trait Extra = Send + Sync> = Box<dyn Error + 'static + Extra>;

Used as AnyError<?Sized> to strip the extra bounds.)

See:

3 Likes

I'm inclined to agree with you about including Send + Sync: we aren't forcing anyone to use this, they can continue to define their own type alias like type Error = Box<dyn std::error::Error + 'static> if they don't want Send + Sync.

Another benefit:

Suppose someone knows very little about rust but has heard that the csv crate can work with very large csv files fast. They go to the documentation and see:

fn example() -> Result<(), Box<dyn Error>> {
    // Build the CSV reader and iterate over each record.
    let mut rdr = csv::Reader::from_reader(io::stdin());
    for result in rdr.records() {
        // The iterator yields Result<StringRecord, Error>, so we check the
        // error here.
        let record = result?;
        println!("{:?}", record);
    }
    Ok(())
}

If instad they see

fn example() -> Result<(), AnyError> {
    // Build the CSV reader and iterate over each record.
    let mut rdr = csv::Reader::from_reader(io::stdin());
    for result in rdr.records() {
        // The iterator yields Result<StringRecord, Error>, so we check the
        // error here.
        let record = result?;
        println!("{:?}", record);
    }
    Ok(())
}

then they don't have to know about Box and the heap, and also dyn and trait objects, allowing them to focus more on the important part of the example.

2 Likes

Edit:

The big problem is the same as with Box<Error> and failure::Error: they can't implement both From<E: Error> and std::error::Error, which is probably a deal breaker for putting this in std. failure works around this with an awkward Compat type.


If this is happening, I would strongly advocate for going for an implementation close to failure::Error, notably (in addition to the already mentioned opaque thin pointer) , make DynError automatically capture a backtrace.

Personally I'm very much in favor of this. It would give us a standard dynamic error type that can be used in docs, examples, in code that doesn't require typed errors, ... and that captures backtraces for easier debugging.

It would be a big boon for for new users : you can tell them to just use DynError until they gain a better understanding of what error types to use, and they get backtraces automatically.

The danger is of course a proliferation of DynError in contexts where it is not appropriate, eg in libraries. That could hopefully be somewhat countered with documenation.

2 Likes

That would be awesome.

Was this already RFCed?

(Nope, it's one of my long-term Haskell-inspired ideas which I'm tracking at GitHub - Centril/rfc-trait-parametric-polymorphism: Planning, scheming and designing of {-# LANGAUGE ConstraintKinds #-} for Rust -- feel free to dump use cases and whatnot there. It's a major typesystem addition so we have to think about coherence, inference, etc. hard.)

And perhaps most importantly, whether it's worth it.

6 Likes

Is this something that would be solved by speicalization? I'm trying to get my head around the problem, is it to do with the fact that implementing both From<E: Error> and Error could lead to situations where the compiler would make a From<From<From<From<From<...>>>>?

Specialization can solve this if we have the lattice rule.

1 Like

How comfortable would we feel having the stdlib use specialization for this? Since the stdlib can use unstable features on stable, this would let us get the advantages of failure::Error / fehler::Exception in both lib and bin crates with full interop (unlike those crates). That would be a huge advantage for moving this proposal forward in my mind.

The current rule for specialization in the standard library is that the API surface must be fully expressible without specialization. (That is, every specialized impl must be fully covered by the general impl.)

What specialization would you use for DynError?

1 Like

The current rule for specialization in the standard library is that the API surface must be fully expressible without specialization. (That is, every specialized impl must be fully covered by the general impl.)

What specialization would you use for DynError ?

From earlier in the thread

The big problem is the same as with Box<Error> and failure::Error : they can't implement both From<E: Error> and std::error::Error , which is probably a deal breaker for putting this in std. failure works around this with an awkward Compat type.

From my experience with failure::Error, a boxed Error seems limited in use to just applications without specialization but isn't obviously so, creating a trap for developers to make mistakes.

Note that Try's design isn't finalized, so there's also the opportunity here to choose a different ? desugaring that would all us to bypass this kind of problem.

1 Like

Is there a way to do that though that wouldn't break a whole lot of current code that relies on simple From impls for errors?

3 Likes

I'd like to make progress on this idea, but the consensus seems to be that this is blocked on specialization. Is that a fair description?