How to box an error type retaining `std::error::Error` (only) when std is enabled?

Suppose I have a type

struct ListNode<T> {
    data: T,
    next: Option<Box<Self>>,
}

and I want to parse this as a string, so I define the following error type

enum Error<T: FromStr> {
    TooFewNodes,
    Parse(<T as FromStr>::Err),
}

impl<T: FromStr> FromStr for ListNode<T> {
    type Err = Error<T>;
    // ...
}

Now, if I want to implement std::error::Error on my error type, while retaining nostd compatibility, I can do so by

#[cfg(feature = "std")]
impl<T: std::error::Error> std::error::Error for Error<T> {
    // ...
}

Ok, but now I want to get rid of the <T> bound on the error type, because it results in multiple copies of the type (and every type that contains it) plus it's annoying to deal with. I can assume I have an allocator because otherwise none of the recursive types that are fundamental to my library can work. The standard way would be to change my error type to

enum Error {
    TooFewNodes,
    Parse(Box<dyn std::error::Error>),
}

Except that this type obviously can't be defined in a nostd environment. Ok, I can encapsulate the box inside an opaque type which conditionally holds either a dyn Error or a dyn Display, say. This type can exist in a nostd environment but I can't actually construct it in the way that I want! Ultimately what I want is

  • If std is off, the type should always hold a dyn Display
  • If std is on, the type should hold a dyn Error (if the underlying error type implements Error).

The problem, of course, is that anyone who tries to use a non-std::error::Error error type with my library will find that my code compiles for them in nostd mode but not in std mode, i.e. my std feature is not additive.

Judging from the lack of solutions in thiserror and anyhow where dtolnay (who has done more than anyone to abuse the language in the direction of making error traits usable) I assume this is just impossible. But this is a huge ergonomic hit, that I either need a <T> parameter on pretty-much every error type in my library (and my consumers' libraries) or I need to throw away the error information from T.

Define your own error trait, and conditionally expose a conversion method such as

#[cfg(feature = "std")]
fn as_error(&self) -> &dyn Error;

@paramagnetic I can do that, but I need a default implementation of the method so that people who don't implement it won't fail to compile with std on (when they do compile with it off). It's not clear what that default impl would be.

So I could change your method signature to return Option<&dyn Error> and have a default impl that returns None, but now I need everybody to manually implement my trait for their error types. But what if their error types are e.g. core::num::ParseIntError? Do I insist that core implement my error trait? I'd rather insist that they implement their own error trait..

1 Like

Note that std::error::Error is just a re-export of core::error::Error. What limitations are you experiencing on nostd?

2 Likes

@steffahn if anybody tries to use Error from core they will get this error

error[E0658]: use of unstable library feature 'error_in_core'
 --> src/main.rs:4:6
  |
4 | impl core::error::Error for Hmm {
  |      ^^^^^^^^^^^^^^^^^^
  |
  = note: see issue #103765 <https://github.com/rust-lang/rust/issues/103765> for more information
1 Like

Ah, I missed that. It’s going to stabilize in a week though. (with 1.81, releasing September 5th)

3 Likes

Hallelujah! But unfortunately that doesn't help me for multiple years at least until I can assume all my consumers have upgraded their compilers. (And I can't even conditionally compile a core::error::Error bound on <T as FromStr>::Err based on compiler version, for much the same reason that I can't do it based on cargo features. Conditional compilation based on versions also needs to be monotonic so that peoples' compilations won't break when they upgrade compilers.)

So I would still like a pre-1.81 solution to exist.

1 Like

I'm probably confused, but doesn't the above mean that you're depending unconditionally on the alloc library and therefore you can use alloc::boxed::Box in your API?

1 Like

Yes, I can.

So then you can use the type below unconditionally, but with alloc::boxed::Box?:

It's that last sentence that I'm probably confused about, since you require alloc.

The box contains a dyn std::error::Error. The std::error::Error trait does not exist in nostd.

Ah, sorry, I was fixated on the Box.

I'll probably stick my foot in my mouth again here.

As you've said yourself, it is not possible to use Rust's Error type in the current version of Rust with no_std. You don't want to rely on 1.8.2 and you want to support no_std, so you can't use Error. This is not a big deal in my view, because currently it is common to have Result error types that don't implement Error. It is not a great situation, but it is the current situation.

If you want a type erased error, you could just add this to your own error type:

fn as_dyn(&self) -> &dyn Display;

True, it is "no big deal" to have types which cannot implement std::error::Error, though I wonder what the purpose of the trait is, if it's impossible to use for nearly 10 years after Rust 1.0.

But yes, I am just going to use dyn Display for now (or maybe a dummy trait which also gets me Debug).

Yes, I didn't mean to diminish your complaint, I think it is a valid one.

Sorry, I don't get why there would be an error while compiling with the feature on. The method would exist only with the feature.

That requires every implementor to also conditionally implement the method in order to match the trait definition, which violates the purely-additive principle for features.

I was under the impression that this error handling mechanism would be intended to be used by types in OP's crate, and not as something 3rd-party code would need to implement.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.