'In Rust, what happens if main function returns Err?

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).



Solution 1:[1]

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

Solution 2:[2]

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).

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Dogbert
Solution 2 ynn