25

According to The Rust Reference,

If a main function is present, (snip), and its return type must be one of the following:

  • ()

  • Result<(), E> where E: Error

but it doesn't say what happens when main() returns (), Ok(()) or Err(<value>).

As far as I tested,

() Ok(()) Err(<value>)
Exit Status 0 0 1
Additional Behavior - - Error: <value> is printed to stderr

Are these behaviors defined, explicitly explained or guaranteed in some documentation? In particular, can I assume

  • a program always exits with 1 status when main() returns Err(<value>)?

  • the error message displayed when main() returns Err(<value>) is always of the form Error: <value>?


Notes:

  • I want some sort of documented guarantee rather than an empirical explanation. This is why I added #language-lawyer tag.

  • This question is not about When should I use () and when should I use Result<(), E>? or such. One can find answers (or at least hints or criteria) to such questions in many documentations or tutorials, as you know.


Updates:

Termination trait is finally stabilized in Rust 1.61.0 (source).

ynn
  • 3,386
  • 2
  • 19
  • 42
  • 2
    The [`Termination`](https://doc.rust-lang.org/std/process/trait.Termination.html) trait plays a central role here. Unfortunately, it doesn't document this behavior either. My guess would be that it's simply currently not documented/guaranteed anywhere except in those `Termination for Result` impls. – Lukas Kalbertodt Apr 29 '22 at 07:43
  • 1
    And here is the [RFC](https://github.com/rust-lang/rfcs/blob/master/text/1937-ques-in-main.md) for the `Termination` trait. – rodrigo Apr 29 '22 at 07:47
  • 1
    Well the `Termination` docs do state that: "_The default implementations are returning `libc::EXIT_SUCCESS` to indicate a successful execution. In case of a failure, `libc::EXIT_FAILURE` is returned,_" so that part can probably be relied on (as much as any unstable feature can be). The error printing OTOH appears undocumented (although implied by the `E: Debug` bounds on the `impl Termination for Result<…>` declarations). – Jmb Apr 29 '22 at 07:49
  • 1
    Since the behavior is documented only in an unstable type (and even then not fully, e.g. it delegates to the lower-level `EXIT_SUCCESS` and `EXIT_FAILURE` and fails to document the exact output), you can't assume either of those things, and should format the output and call `std::process::exit()` yourself. Of course, the behavior is extremely unlikely to change _in practice_, as that would be a backward-incompatible change, but from a language-lawyer perspective, you simply don't (yet) get the guarantees you're asking about. – user4815162342 Apr 29 '22 at 12:20

2 Answers2

24

The behavior of different return values from main is defined by the std::process::Termination trait:

trait std::process::Termination

A trait for implementing arbitrary return types in the main function.

This trait is documented to return libc::EXIT_SUCCESS on success and libc::EXIT_FAILURE on error.

The default implementations are returning libc::EXIT_SUCCESS to indicate a successful execution. In case of a failure, libc::EXIT_FAILURE is returned.

But those values aren't guaranteed to be 0 and 1 on non-POSIX systems.


As for printing the error message, Termination requires E: Debug and does print the Debug impl to stderr, but I don't believe it's guaranteed to stay exactly the same.

impl<E: fmt::Debug> Termination for Result<!, E> {
    fn report(self) -> ExitCode {
        let Err(err) = self;
        eprintln!("Error: {:?}", err);
        ExitCode::FAILURE.report()
    }
}

Source

Dogbert
  • 212,659
  • 41
  • 396
  • 397
8

This behavior is controlled by the std::process::Termination Trait, which was added in RFC 1937. In particular, the "hidden" lang_start() function - which calls main() - looks roughly like:

fn lang_start<T: Termination>(
    main: fn() -> T,
    argc: isize,
    argv: *const *const u8
) -> !

that is, main() can return any T: Termination. There are implementations of Termination in std for !, (), std::process:ExitCode, and some Result<T, E>-variants where E: Debug. This is why you can return (), Ok(()) and others from main().

To your question, language lawyer mode: The exact behavior of any program that relies on Termination is not strictly specified by the language itself. It is part of the std implementation, not part of the language reference. That means that the same program might behave differently when compiled with different versions of the compiler (binding to different std versions). The exact behavior for printing errors in the Err case is documented but not specified. As RFC 1937 explicitly looks for POSIX-like behavior, you can be reasonably assured that the program will not behave in wildly suprising ways (e.g. exit with status 0 in an Err-case).

ynn
  • 3,386
  • 2
  • 19
  • 42
user2722968
  • 13,636
  • 2
  • 46
  • 67