`main` returning `Result` idiom

EDIT, current status

The original post was about how we should stop making returning Result from main an idiom when it is undesirable in real programs. Now, the discussion has moved to be about actually fixing Rust's std behaviour (changing impl Termination for Result and without breaking backwards compatibility).

Original Post

I've noticed the Rust documentation, the Rust book, and other resources are very fond of mentioning that main can return Result. Yes, it can return any type that implements the Termination trait.

As far as I understand it, Result in return exists to make code snippets simpler, mostly for docs and related areas, along with some sort of design consistency choice.

Only from this perspective does it make sense that Result's implementation of the Termination trait is to debug print the error object. A quick search shows this has been the subject of several discussions on this forum alone, e.g., 1 2 3 .

The posts effectively consist of the poster having written,

fn main() -> Result<(), Box<dyn Error>> {
    let str = read_to_string("file.txt")?;
    println!("{str}");
    Ok(())
}

and they are bothered by the format of the output during errors.

These forum topics are somewhat silly to read, as they consist of a mix of people harassing the original poster for not just doing basic error handling in main and other people posting their own boilerplate code they use in every program to solve this micro-problem:

  1. Make main always call a separate run or program or _main function and then abort on error or return ExitCode (normal)
  2. Return your own error type in main and make its fmt method just print the message rather than the expected fmt behaviour (weird, but commonly suggested)
  3. Create your own Termination type

So what is the problem? Needing a couple lines of code to display something in a reasonable format is not a problem. What could be a problem is faulty idioms and documentation. The reason it is a problem in this case is only because we are talking about the most basic possible program you could possibly write.

The aforementioned discussion post examples often use abort() and then go "oh, wait." abort() might not release resources and the docs say you should avoid using the function when you can use regular control flow and return an ExitCode,

Ok, now let's look at the docs for ExitCode::FAILURE. You'll see the following notice:

If you’re only returning this and SUCCESS from main, consider instead returning Err(_) and Ok(()) respectively, which will return the same codes (but will also eprintln! the error).

Wait a minute, didn't we all just agree returning Result in main is essentially just for examples? We know this is a serious language and we want the language to take itself seriously too.

P.S. I will of course make a PR to change at least that last piece of documentation unless other's totally disagree with me on this subject (see EDIT)

7 Likes

That wasn't the original intention, but due to the (flawed IMO) implementation choice to not require Display, it is the effective result (heh)...

...unless you do something like (2) or use a crate that does so (like anyhow), which can be fine.

Change it to what?

2 Likes

It would be very good if this behavior could be changed to a more sensible one of Display-printing the entire error chain. The problem is making that change without breaking existing programs.

3 Likes

One issue is that () does not implement display so this would break:

use std::process::Termination;
fn main() -> impl Termination {}

They're referring to the implementation of Termination for Result<(), E> where E: Debug. Ideally, it would require E: Display instead.

2 Likes

Can this be implemented using the unstable try_as_dyn?

In the impl Termination for Result<T, E, we check if E implements Display, then print using Display, falling back to Debug output if it doesn't. We could even remove the E: Debug bound, which could increase flexibility

#[stable(feature = "termination_trait_lib", since = "1.61.0")]
impl<T: Termination, E: fmt::Debug> Termination for Result<T, E> {
    fn report(self) -> ExitCode {
        match self {
            Ok(val) => val.report(),
            Err(err) => {
                if let Some(err) = core::any::try_as_dyn::<_, dyn Display>(&err) {
                    io::attempt_print_to_stderr(format_args_nl!("Error: {err}"));
                } else {
                    io::attempt_print_to_stderr(format_args_nl!("Error: {err:?}"));
                }

                ExitCode::FAILURE
            }
        }
    }
}

There is a PR to remove 'static bound on try_as_dyn which would make this work: https://github.com/rust-lang/rust/pull/150161

4 Likes

IMO this is the surprising behaviour that try_as_dyn and specialization should not be used for.

2 Likes

As far as I understand it, Result in return exists to make code snippets simpler, mostly for docs and related areas, along with some sort of design consistency choice.
...
Wait a minute, didn't we all just agree returning Result in main is essentially just for examples?

There is another, more serious advantage to returning Result from main, rather than handling errors and calling exit(). It ensures that Drop will be called as everything unwinds nicely. (At the cost of not allowing you to choose your own exit codes)

std::process::exit warns:

"Note that because this function never returns, and that it terminates the process, no destructors on the current stack or any other thread’s stack will be run. If a clean shutdown is needed it is recommended to ... simply return a type implementing Termination ... from the main function and avoid this function altogether"

Sometimes you may want this as a fast shutdown is nice UX, the OS will handle memory clean-up, and there's nothing that's a security risk. But ... if anything in your app uses RAII and expects a nice shutdown, then not returning Result can have side-effects which you may not have thought of.

I ran into this recently and (shameless plug, please don't shoot me) created exit_safely - crates.io: Rust Package Registry to make things a little easier (it uses some unstable features and is still WIP but does what it says on the tin)

I was expecting people to be defending Rust's current behavior more.

I was also basically thinking that any change to Result's report() would be too backwards incompatible. But what you all are pointing out is that the std only specifies that the error type must implement Debug, it does not yet guarantee a particular format of the output.

Looks great to me.

ExitCode also works fine. Nice crate.

1 Like

Would there even be any significant breakage? The Error trait already implies Display, and I don't think that there are many uses of "returning Result from main" in the first place, due to reasons outlined in this thread.

I'm not certain who you're debating with given that we all seem to be agreeing about this issue.

But in honor of your question,

Would there even be any significant breakage?

I'm sure you can think of a program that is parsing the output of another program and designed to handle a particular format for its stderr. Yeah, they shouldn't be returning Result and expecting the error format to be consistent and go to the effort to parse the debug struct, but that doesn't mean someone isn't doing it.

Still, you're absolutely right. It's true, I'm very conservative when it comes to backwards compatibility and very supportive of language stability, and so I didn't want to be the one to throw this out there. But since others have suggested to just change it, I am in full support :stuck_out_tongue:

This is a bad idea as most types do not impliment display and only error. a.k.a error types

Types that implement std::error::Error must implement Display, actually.

oh you right.

Does the try_as_dyn trick work through dyn objects? I've seen (and used) Result<T, Box<dyn Debug>> as a budget anyhow more than a few times, and the docs imply the trait resolution is completely static.

This isn't a show stopper or anything, since the behavior change being visible in the types is a good thing, but it doesn't seem like there's a good way to represent the new behavior as a dyn object; you might spell it as dyn Debug + ?Display but I'm not sure there's any appetite for a type like that.

Probably the new budget anyhow would be just a "DebugAsDisplay" wrapper for types without Display, which of course you can already do.

No (and that would require runtime generation of vtables I believe).

That's what I tend to do, or do it in main and have some real_main instead.[1]


  1. Which is why I consider the Debug based implementation a mistake -- one of the goals was to get rid of requiring things like real_main for good UX CLI tools, and going with Debug goes directly against that. ↩︎

1 Like

The Error trait is not required by the implementation of Termination for Result<T, E> though.

Yes, but how often do you see the Err in a Result, especially one that's bubbled up all the way to main, not be an Error in practice?

Anyhow is incredibly popular and doesn't implement Error (for reasons). I also mentioned above using dyn Debug as a hack for getting simple "use anything as an error" approach.

Regardless, this would be a source breaking change, which has an extremely high bar even across editions, so "being common" is not the question here.

Meh, even the worst possible breakage caused by fixing this misdesign would only break leaf binary crates, never any library dependents. Drawing the line for what is considered breakable, in a way such that making this feature usable becomes impossible, you may as well just archive the rust repository.

1 Like