Termination for Result<_, E: Debug>

The Termination trait used to support fn main() -> Result<(), Fail> is only implemented for Result<_, E: Debug>, rather than Result<_, E: Error>.

Problems:

  1. Rust could have an option to remove "useless" Debug implementations (for file size savings and/or implementation hiding), but failure reporting from main is more of a mandatory user interface, rather than only an optional developer-oriented use of Debug, so it seems disproportionally affected by a debug output flag.

  2. The Debug output is not the prettiest way to print an Error.

  3. The anyhow has a bit of a hack that takes advantage of the E: Debug implementation to format the errors nicely via its Debug impl. Switching the Termination implementation to use Display would improve user-friendliness of the error output — except for anyhow.


I wonder if this is fixable.

  • Could Termination impl be changed to require Result<_, E: Error>? (most likely via some edition-specific hack)

  • If it cannot be changed, can it use the specialization feature to magically handle E: Error despite not having it in publicly-visible bound? That would be an observable use of specialization, but OTOH it only runs after main returns, so maybe it's not a big problem?

  • is "breaking" anyhow a problem? What if libstd printed Error in the same way, with the source() chain?

3 Likes

The unstable std::error::Report does this. By default on a single line similar to {} on anyhow, but if you call .pretty(true) on it, the error will be printed on multiple lines similar to {:?} in anyhow. The Debug impl of Report forwards to the Display impl for compatibility with Termination.

I think we can use two Termination traits and use one or the other for fn main() depending on the edition. Termination has been stabilized though, so the difference would be user visible and make manual Termination impls only work in older editions if the Termination trait of the older editions was implemented but not the newer version.

1 Like

Box<dyn Error> and anyhow::Error (and anything else with a From<E: Error> implementation) don't implement Error, so this would disallow

fn main() -> Result<(), Box<dyn Error>> {
    Ok(())
}
2 Likes

It might be possible to support both an old and a new Termination trait without an edition change. And given that not all types can implement Error, I think we will want to support non-Error types in new editions.

It's possible for macros to do type fallback via a series of traits, by taking advantage of multiple levels of automatic dereference. See Rust Playground for an example. (Credit to @epage and clap, where I learned this technique from.)

I don't know if there's any way to do this without a macro. But if a macro can do this, we should be able to get rustc to do the equivalent when processing the user's definition of main.

1 Like

Some history and links to prior discussions (for Result<_, E: Display>):

There was more contention around the goal of the Result implementation of the Termination trait than I remembered. To summarize coarsely, but hopefully not too far off base, there were two camps:[1]

  • It's for hacky script-likes and you shouldn't use it for anything polished, so the E bound should be Debug
  • It's not just for hacks, and even for hacky script-likes, you never want to see typical Debug output as the result of running a CLI, so the E bound should be Display

Lang team (primarily Boats and Niko apparently) made the call and they were in the first camp, so Debug is what we got. Interestingly, Boats eventually decided this was the wrong decision.

There's a lot of discussion starting here and running forward, including some discussion of the viability of changing the (stabilized) implementation. Mainly "specialization" and "another trait". There's another thread here.


  1. after the downsides of E: Error were realized anyway ↩︎

3 Likes

Credit to Generalized Autoref-Based Specialization · Lukasʼ Blog which is what I based the clap design around.

2 Likes

Ugh, what a bummer about Box<dyn Error> (and 3rd party equivalents) not implementing Error. These are the most likely types to be used in main (BTW, it looks like the unstable Report type has an issue with this too, and needs to special-case Box<dyn Error>).

So this makes explicit user-facing E: Error bound out of the question (I assume specialization/orphan rules won't change anytime soon to allow all errors to implement Error).


But what about magically supporting errors, while keeping it E: Debug in the public API?

// specialized
impl<E> Termination for Result<(), E> 
    where E: Debug + Into<Box<dyn Error>>

This seems to be compatible with most error-like types, including anyhow.

2 Likes

Adding this specializing impl is a breaking change:

#[derive(Debug)]
struct MyError<'a, 'b>(PhantomData<(&'a (), &'b ())>);

impl<'a, 'b> std::fmt::Display for MyError<'a, 'b> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str("display MyError")
    }
}

impl<'a, 'b> Error for MyError<'a, 'b> {}

fn foo<'a, 'b>() {
    Err(MyError(PhantomData::<(&'a (), &'b ())>)).report();
}

works without the specializing impl, but errors with it:

error: lifetime may not live long enough
  --> src/main.rs:49:5
   |
48 | fn foo<'a, 'b>() {
   |        -- lifetime `'a` defined here
49 |     Err(MyError(PhantomData::<(&'a (), &'b ())>)).report();
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ argument requires that `'a` must outlive `'static`

error: lifetime may not live long enough
  --> src/main.rs:49:5
   |
48 | fn foo<'a, 'b>() {
   |            -- lifetime `'b` defined here
49 |     Err(MyError(PhantomData::<(&'a (), &'b ())>)).report();
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ argument requires that `'b` must outlive `'static`

help: the following changes may resolve your lifetime errors
  |
  = help: replace `'a` with `'static`
  = help: replace `'b` with `'static`
1 Like

Switching to E: Debug + AsRef<dyn Error> makes it work with the lifetimes, but unfortunately that picks up fewer error types. Another solution just for the fmt-debug option could be E: Debug + Display.


Oh! A simple lifetime change makes Into work:

impl<'a, E> Termination for Result<(), E> 
    where E: Debug + Into<Box<dyn Error + 'a>> {

Yeah, that solves the compilation failure. I just noticed that this uses the unsound full specialization than min_specialization. I don't know if it is exploitable here, but it is a bit worrisome.

Turning something into a Box<dyn Error> would force an allocation, though. That seems undesirable to force on every program that returns an error from main.

We only allow min_specialization in std.

I can't find documentation on what's different about min_specialization. The Unstable Book only links to a tracking issue, which links to an unimplemented RFC, and a PR that is too implementation-focused that it's hard to understand what is the user-facing side of the feature.

The example from min_specialization.rs in rustc doesn't compile:

#[rustc_specialization_trait]
trait AlwaysApplicable: Debug { }
impl<T> Tr for T { }
impl<T: AlwaysApplicable> Tr for T { }

error: specialization impl does not specialize any associated items
--> src/lib.rs:14:1
14 | impl<T: AlwaysApplicable> Tr for T { }

min_specialization is supposed to prevent specializing on lifetimes. Into<Box<dyn Error + 'a>> can be conditionally implemented only for specific lifetimes and thus is rejected by min_specialization.

I tried to exploit the use of full specialization, but in the process I found another case of adding the specializing impl causing compilation to fail:

#[derive(Debug)]
struct MyError<'a>(&'a String);

impl std::fmt::Display for MyError<'static> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str("display MyError")
    }
}

thread_local! {
    static FOO: RefCell<Option<&'static String>> = RefCell::new(None);
}

impl<'a> Into<Box<dyn Error + 'a>> for MyError<'static> {
    fn into(self) -> Box<dyn Error + 'a> {
        FOO.with(|foo| *foo.borrow_mut() = Some(self.0));
        "foo".into()
    }
}

fn main() {
    foo(&String::from("foo"));
}

fn foo<'a>(s: &'a String) {
    Err(MyError(s)).report();
}

works without the specializing impl, but with I get:

error[E0521]: borrowed data escapes outside of function
  --> src/main.rs:59:5
   |
58 | fn foo<'a>(s: &'a String) {
   |        --  - `s` is a reference that is only valid in the function body
   |        |
   |        lifetime `'a` defined here
59 |     Err(MyError(s)).report();
   |     ^^^^^^^^^^^^^^^^^^^^^^^^
   |     |
   |     `s` escapes the function body here
   |     argument requires that `'a` must outlive `'static`

(The usage of TLS was an attempt to smuggle an 'a reference out of the Into impl as 'static. It is not be load bearing to the compilation failure.)

This seems like the obvious path if we really wanted to do a change.


I'm still torn about whether it's worth doing, though. It feels somewhat like println! to me -- where println! is a very important thing to exist and be in the prelude, but in "real" code you probably shouldn't be using it because you should be writing to a Write of some sort so that it's testable and such, and just happens to be always called with stdout in the normal codepaths.

If I really care about what I show on output for stuff, it feels like there's no way that the compiler one can be expected to be that, and thus I shouldn't use this. And if we're willing to let std change exactly what it shows -- which I think would be reasonable -- then it feels more like a Debug than a Display.

1 Like

I think we can do better than that, and one day I hope we end up with something more like eyre::Report, which is substantially customizable.

It's just not obvious to me that "substantially customizable" works well in std, since it's hard to have a reasonable limit on customization knobs.

When a crate can just offer an #[eyre::main] or anyhow::main!(…) or whatever, that seems like a great way to offer loads of control over the exact format, and way faster turnaround on offering new options/knobs than std can do.

Which leaves the Termination more about "well we want something to make it easy to use ? in main in your first rust programs" where having developer-focused output makes sense.

(I suppose this might also be another place where it'd be nice to have pluggable implementations of stuff. If we have a generic feature like global_allocator we could have a error_reporter to let people pick their strategy…)

Can’t this be achieved today by returning Result<_, ErrorReporter> from main and providing appropriate From and Debug/Display impls for ErrorReporter?

Any code defining some custom wrapper type or trait implementation for customizing the default output would be longer and more complicated than if let Err(e) = inner_main() { customize however you wish }.

I think this fact was a factor why Termination went for Debug in the first place. Custom error printing doesn't need to be tied to awkwardness of a black box code running after main().

However, the choice to use Debug for reporting of CLI program failures blurred the line between developer-oriented output and user-facing output. I want to add option to Rust to disable Debug in binaries that don't want to expose dev internals to users, but this is breaking main termination, and that feels to me like going too far and affecting user-facing output (even if that output is not nicely formatted). So I'm mainly concerned about a fallback for lack of Debug, rather than adding fully featured customizable error printing machinery.

2 Likes