Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Return Result from main #1176

Closed
Ericson2314 opened this issue Jun 24, 2015 · 69 comments · Fixed by #1937
Closed

Return Result from main #1176

Ericson2314 opened this issue Jun 24, 2015 · 69 comments · Fixed by #1937
Labels
T-lang Relevant to the language team, which will review and decide on the RFC.

Comments

@Ericson2314
Copy link
Contributor

Rust's current error handling story is quite good for programming in the large. But for small programs, the overhead of matching the outermost Result, printing the error, and returning the proper error code is quite tedious in comparison to temptation of just panicking.

Furthermore, interactive tools shouldn't print a message implying the program is broken on bad manual input, yet such an "internal error" is exactly what the panic message applies. So there is no nice, succinct way to handle input errors with bubbling results or panics.

Allowing main to return a result would make our error handling story both compositional and succinct on programs on all sizes. Furthermore, should there be any platform-specific requirements/idioms for error-codes, etc, we can handle them transparently. While this does make main a bit more complex, there is already a shim between start and main with both C and Rust, so we're already down that rabbit hole.

I was told by @eddyb this was already a wanted feature, but @steveklabnik didn't see an open issue so I made this.

@steveklabnik
Copy link
Member

I know @chris-morgan has thought about this a bunch and has some concerns

@nagisa
Copy link
Member

nagisa commented Jun 24, 2015

I really remember a lot of discussion about this happening, but can’t find anything either, sadly.

Off the top of my head some points raised during these discussions:

  • Unclear what to do with Ok value, unclear what to do with Err value. What both of these variants may contain?
  • Return an exit code instead;
  • Return Option instead, if None, exit successfully. A bit counter-intuitive that None is a success and Some is a failure.
  • Should main become somewhat special and the only overloadable function in rust? What about people who simply don’t care?

@Ericson2314
Copy link
Contributor Author

  1. Restrict to Result<(), SOMETHING>
  2. Should return appropriate exit code, and no panic-like extra text because IMO if the error can be propagated to the top it is not an internal error.
  3. Weird, and doesn't help promote idioms
  4. I've long argued we need parametrized crates like ML functors. Then any crate can call a not-yet defined function. Then any library can define std::start and export it's own hook (like SDL in C). Indeed std can use this to define start and call main. [As an side bonus, this would also allow log avoid using allocation and dynamic dispatch.] This is better than some main-specific hack, but IIRC this route has been resisted before.

@eddyb
Copy link
Member

eddyb commented Jun 25, 2015

@nagisa Rust has overloading via traits, so this could work:

trait MainReturn {
    fn into_error_code(self) -> i32;
}
impl MainReturn for () {
    fn into_error_code(self) -> i32 { 0 }
}
impl<E: Error> MainReturn for Result<(), E> {
    fn into_error_code(self) -> i32 {
        if let Err(e) = self {
            write!(stderr(), "{}", e).unwrap();
            1
        } else { 0 }
    }
}
#[lang="start"]
fn start<R, F>(argv: *const *const c_char, argc: u32, main: F) -> i32
   where R: MainReturn, F: FnOnce() -> R
{
    with_runtime(argv, argc, || {
        main().into_error_code()
    })
}

@daboross
Copy link

I'd argue that it is very easy and simple to just .unwrap() a Result returned from a try_main() function.

fn main() {
    try_main().unwrap();
}

fn try_main() -> Result<(), Box<Error> {
   // ...
}

To explicitly do this is just 4 extra lines of code - I would argue that adding this implicitly is just not worth it.

@Ericson2314
Copy link
Contributor Author

@daboross Sorry, but did you read my second paragraph? That will print clutter the actual error with all the "thread main panicking at" business which is inappropriate for handling bad user input. It also sticks you with a single error code.

@daboross
Copy link

@Ericson2314 I guess it isn't entirely the best for some cases like bad user input - but I think it's totally appropriate for other errors which really are internal errors. One problem with having main() implicitly handle these errors is that it would be unclear what, if any, text would be included before/after the error.

With error codes, it can really be very simply solved by a single match or if let statement and then a call to exit() manually.

It may turn out to be good to implement this, or not, I'm just here stating my opinion from my experience working with creating programs in rust.

@Ericson2314
Copy link
Contributor Author

By returning a Result, a function is "kicking the can", effectively saying that the error is not it's fault. It follows then that any error error returned by main is the fault of whoever spawned the program and not the program itself. Sometimes internal errors get bubbled up a bit, but those should all be unwrapped at some point.

I totally agree that trying that it is not obviously clear how the error in a result should be printed, but since this is for programming in the small, and since one can handle it themselves, I find it more tolerable than I would otherwise if libstd just picks something.

@yberreby
Copy link

yberreby commented Jul 1, 2015

👎
A wrapper function can do the job very well, and this ties main() to Result: what if we want to use another type? More special cases?

I'd argue that adding 'convenient' special cases is not a very good idea in the long run; it increases complexity and coupling between std and the language. Binary crates should choose their own error handling strategy, be it by writing it themselves or using one from crates.io.

Rust is not a scripting language. I think we should strive to avoid too much features designed for narrow use-cases bolted on it. Weren't ~ and @ removed because they could be implemented in libraries?

@ArtemGr
Copy link

ArtemGr commented Jul 1, 2015

When a function returns an error it thus handles the responsibility of doing something about the error to it's caller.

For example, when a function that reads a file returns an error, the caller decides what to do about it, to print an error or to ask the user for another file name or to skip to the next file or to panic, etc.

This chain of responsibility usually ends at main. While other functions can delegate the responsibility of handling an error to the calle, the main can't, cause nobody else has any idea of what to do about the error.

Should we print a message or simply return a -1 error code to the OS? What about a GUI program, should it display a error message "dialogue"? Only the main knows this.

@eddyb
Copy link
Member

eddyb commented Jul 1, 2015

@filsmick There is already coupling between std and the language - check out std::rt::lang_start.
It's also quite fragile and prone to race conditions due to the use of a global "exit status".

If the trait bounding the return type of main is exposed from libstd then any crate can write a novel implementation for a type of its own.

@BlacklightShining
Copy link

I agree with @ArtemGr. main() should be the top of the chain for bubbling Results. I do think, however, that it should return an i32 (or a u8, maybe, given that exit statuses are effectively u8s on *nix). Currently, we're advising people to do silly things like fn main() { std::process::exit(main2()); } fn main2() -> i32 { /* useful stuff goes here */ }—I hope that's not seen as a permanent solution.

At the very least, there should be some platform-agnostic mechanism to cause the program to exit gracefully (unwinding stacks, dropping structs, etc) that could be used as a drop-in replacement for returning from main().

@eddyb
Copy link
Member

eddyb commented Sep 25, 2015

@BlacklightShining That could be achieved with a type wrapping the exit code and implementing MainReturn - std could provide that type with platform agnostic helpers.

@droundy
Copy link

droundy commented Dec 11, 2015

This sounds to me like a problem that is nicely handled by a utility function in the standard library, which could be named something like exit_on_error, and would accept a FnOnce as its sole argument. This could actually be useful either as main or as a convenience function for cases where you simply want to exit on errors, and don't want to bother propagating then up the stack.

@ticki
Copy link
Contributor

ticki commented Dec 11, 2015

I'm not sure what's the pros over the current way

@Ericson2314

  1. Restrict to Result<(), SOMETHING>

That pattern is redundant and can be reduced to Option<SOMETHING>.

@ArtemGr
Copy link

ArtemGr commented Dec 11, 2015

A program usually returns an integer to the operating system. What integer to return and how to interpret it - Rust can not know this, only the program itself knows. (Even though there are certain conventions in UNIX, a program doesn't always have or can follow them).

Callback passed to the exit_on_error could convert the value returned by main into the operating system integers. Returning Result<(), SOMETHING> or Option<SOMETHING> offers no such conversion.

If there's something else that needs to be done in case of the application failure, like displaying a dialogue, submitting a bug report or whatever, callback can do that as well. Though exit_on_error remains, of course, just a syntactic sugar for manually handing the errors in main.

P.S. In fact, we could have a default callback that, for example, prints the error and returns 1 to the operating system, and calling exit_on_error would actually override that callback.

@steveklabnik
Copy link
Member

That pattern is redundant and can be reduced to Option.

No, they're very different.

Result<(), SOMETHING>

I am executing something for its side effects, and it may error.

Option.

I have a thing that may or may not exist. In this case, the thing is an error.

Option is not for result/failure, but for presence/absence.

@rphmeier
Copy link

The key question is what the error type should be. If this were anything but main, my initial reaction would be something like @eddyb's trait-based approach. However, it feels very strange to have a generic entry point.

@ArtemGr
Copy link

ArtemGr commented Dec 11, 2015

I wonder if std::error::Error could work? As in Result<(), Box<std::error::Error>>?

@ticki
Copy link
Contributor

ticki commented Dec 11, 2015

@steveklabnik I see.

@Ericson2314
Copy link
Contributor Author

@ArtemGr Oh don't get me wrong, this is shamelessly inflexible and opinionated for the sake of a little bit of convenience.

@eddyb's trait idea's makes this just a bit less hard-coded and thus a bit less appalling. :), especially if the impls were in a separate crate (but then need new-type for coherence).

@Binero
Copy link

Binero commented Jan 6, 2016

I like @eddyb's proposal, but would change the type depending on the platform.

PlatformReturn could be defined in the std prologue and standardised values could be added to the std as well. This allows for a lot of flexibility. I would consider the only real negative side effect the verbosity of the main function.

// PlatformReturn would be std::PosixReturn on Linux systems for example. 

fn main<R: PlatformReturn>() -> R {
    std::error::posix::Return::Ok
}

@BlacklightShining
Copy link

@Binero fn main() -> impl PlatformReturn;, otherwise the type of R is determined by the caller, not the callee.

@ninjabear
Copy link

This is becoming more complicated than it needs to be, everything understands return codes, main should just be;

fn main() -> i32 {

and whatever magic needs to happen to make the process really return that code.

@ninjabear
Copy link

It doesn't have to be i32, it can be a wrapper - but Result<.. is going to be complicated to chain processes together, here's the return codes for wget and the return codes for grep. My point is this - returning Result doesn't leave me with a way to return a number like this, and it could interfere with any number I set.

@eddyb
Copy link
Member

eddyb commented Mar 29, 2016

@ninjabear It can be as simple as return Err(ExitCode(5));, and with Result, you can even have those propagate around your code via try!/?, so you don't have to keep all of the codes in main.

@ninjabear
Copy link

@eddyb - that's one of the better compromises I think, however nonzero results aren't necessarily errors - for example grep returns 1 if no lines were found - it's kind of up to the caller to decide if it's an error or not

@eddyb
Copy link
Member

eddyb commented Mar 29, 2016

@ninjabear Sorry, I had forgotten the generalized approach I mentioned in my first comment here:

// In libstd:
trait MainReturn {
    fn into_error_code(self) -> i32;
}
impl MainReturn for () {
    fn into_error_code(self) -> i32 { 0 }
}
impl<T: MainReturn, E: Error> MainReturn for Result<T, E> {
    fn into_error_code(self) -> i32 {
        match self {
            Ok(x) => x.into_error_code()
            Err(e) => {
                write!(stderr(), "{}", e).unwrap();
                101 // This is what panics, use, I think?
            }
        }
    }
}
// In your grep implementation:
enum GrepStatus {
    Found = 0,
    NotFound = 1
}

impl MainReturn for GrepStatus {
    fn into_error_code(self) -> i32 { self as i32 }
}

fn main() -> io::Result<GrepStatus> {
    // ...
    let file = File::open(path)?;
    // ...
    Ok(status)
}

@ticki
Copy link
Contributor

ticki commented Mar 29, 2016

I'd prefer using plain old integral return values, instead of results. These are simpler, and more expressive.

@BlacklightShining
Copy link

@ticki I'm going to have to disagree with you there. Ok(Found), Ok(NotFound), and Err(some_io_error) are self-documenting and carry clear semantic meanings; 1, and 101 aren't and don't. I'll grant that in general, you can take main() -> 0 to mean the program ran to completion successfully, but that still doesn't give you as much information as Ok(Found). And for any other integer, you have to outright fetch the program's documentation just to determine to what extent it indicates failure.

@Ericson2314
Copy link
Contributor Author

Again the problem is the redundancy in how its used. Either people abandon the ?/try! idiom in main for something ad-hoc, or they make helper functions and main is the same match boilerplate every time.

@ninjabear
Copy link

The redundancy in how what is used?

@ArtemGr
Copy link

ArtemGr commented Apr 26, 2016

The redundancy in how what is used?

I think what Ericson2314 means is that by implementing the integer codes in the errors (e.g. with something like a ToInt trait) one would then be able to reuse the error codes in different executables.

Suppose both the "foo" and the "bar" executables can return the SkyIsFalling error. If the SkyIsFalling error is convertible to the main's return code then one can simply reuse the error in both executables, without making any extra boilerplate in main. A different Rust executable, let's call it "foo_bar_runner", might run "foo" and "bar" and convert their error codes back into Rust errors, such as SkyIsFalling.

@ninjabear
Copy link

thanks @ArtemGr

That idea is predicated on a misunderstanding of what SRP is (completely orthogonal to repeating yourself), cannot be reasonably implemented (there's a reason return codes aren't standardised by operating systems) and provides virtually zero benefits to users (how would I even know if I'm calling a Rust executable?)

@ArtemGr
Copy link

ArtemGr commented Apr 26, 2016

@ninjabear
Ed, I agree in a sense that the idea of binding integer return codes to errors (with something akin to a ToInt trait implementation) will only be useful to a very small subset of users. I imagine something like the Oracle Database error codes, where every error has a unique integer representation.

To a normal program, where exit codes are documented for and specific to a particular executable, binding Rust errors to integer error codes will only be a source of confusion and bewildered workarounds (essentially we'll keep doing the match conversions, ignoring the "feature", but new users will keep spending maybe weeks trying to figure how to make the error codes work in the "official" way).

@BlacklightShining
Copy link

@ninjabear You could use try!() or ? in main() wherever you'd use it outside of main(): when some error could occur that would prevent you from doing what you meant to do. If you have a singular output file, the call to open it might be wrapped in a try!()|? (or at least a match that returns Err(CantOpenOutputFile)).

@ninjabear
Copy link

@BlacklightShining

So, consider an application that opens two files using stdlib. I use try! for both. Which one failed? I have to either make liberal use of map_err or modify my program elsewhere to return the right Err.

Even after I've returned the right Err the user still has no clue what's happened. So now I need to do something basic like printng to the user what the problem was. I now need to use a match or if let anyway to stick a println! in there.

You might say - that's cool we can make non Ok values from main cause a panic! exposing the message. That too is fine, right up until someone says, can you make errors red (or write to a file, switch on a red LED, reboot the application or any other possibility you can imagine).

@BlacklightShining
Copy link

@ninjabear Okay, yeah, in those cases you'd have to use map_err() or match. Still, having all your exit codes in a single impl rather than scattered around main() as magic numbers seems like a Good Thing. Makes writing the man page a little easier. Saves on some boilerplate, too, letting you write the code to write to stderr and exit (or write to a file, or blink the lights, or…) in one place instead of duplicating it to everywhere in main() that something returns a Result. AND it provides a proper mechanism for exiting with a specific status without having to write extra boilerplate scopes or functions or whatever.

@ninjabear
Copy link

@BlacklightShining

There's a ton of other ways to remove magic numbers. Enums? Structs? Constants? All of these are available to you.

I don't think there's anything further I can add to this thread.

@ArtemGr
Copy link

ArtemGr commented May 4, 2016

Still, having all your exit codes in a single impl rather than scattered around main() as magic numbers seems like a Good Thing. Makes writing the man page a little easier.

How is the "single impl" thing going to work?

My guess was the opposite, that you'll have to implement the trait for every error type that main can return and so there will be dozens of impls.

It is much easier to track a single match block (wrapped in a function for reusability) than a dozen impls.

@BlacklightShining
Copy link

@ninjabear Enums sound good! Just one problem: you can't return an enum from main(). Hence this issue.

@ArtemGr You make a new error type specific to your program, implement MainReturn for it, and use map_err() to wrap the various error types into FooProgramErrors.

@ninjabear
Copy link

@BlacklightShining

enum ReturnValue {
    Success = 0,
    Failed = 1
}

fn main() -> i32 {
    return ReturnValue::Success as i32
}

@Binero
Copy link

Binero commented May 8, 2016

I just don't think the -> i32 should be hard-set in the language. What if someone wanted to make a platform which wants another type, like a u32 or a i64?

@ticki
Copy link
Contributor

ticki commented May 8, 2016

@Binero In pratice, that doesn't happen.

@ticki
Copy link
Contributor

ticki commented May 8, 2016

Not to mention that exit is already platform dependent.

@norru
Copy link

norru commented May 19, 2016

Rust n00b here. What is the clean way to have a Rust program return a nonzero value to the operating system? I have read around that the recommended way is to run process::exit() but I seem to have read it doesn't run all the "destructor"/"cleanup" code. that doesn't sound very "clean". Is that right?

@ticki
Copy link
Contributor

ticki commented May 19, 2016

@norru, well there are two possibilities:

  1. Panic, panic can be done through the panic! macro, or other methods panicking (e.g. unwrap). This is not ideal for a lot of reachable code, since the error formatting is not intented to shown to the end-user. Panic will by default unwind the stack and call all destructors (e.g., flushing streams, deallocating memory, and so on). If your program enters an inconsistent state or a logic error, then panic.
  2. Simply using exit, as you mentioned, this will not call destructors, but don't let that stop you. exit gives you full control over output and how it is handled. Since destructors are often critical for well-behaving code, you can do one of two. a) call the destructors manually through drop, which is imported by the prelude. b) make a function acting as the scope, so that destructors are called in prior to the exit, e.g.:
use std::process;

fn start() {
    // Program code here.
}

fn main() {
    // We run the program.
    start();
    // Now that the function is over, the associated destructors
    // (from variables and their children) are called, we can safely
    // exit without getting into trouble:
    process::exit(1);
}

This should be used when you need to show an error message to the user, or otherwise exit the program with a fail code.

The former is prefered for internal error handling, the later for external error handling.

@norru
Copy link

norru commented May 19, 2016

So in the generic case it would look something like this in "pseudo-rust". Is this such an uncommon case in Rust?

fn c_main(args: &Vec<String>) -> i32 {
    if !valid_args(args) { return 2 }
    // resource allocation RAII and other amenities
    let context = init_stuff();
    // do stuff with args
    match context.do_something_with_args(args) {
        Err(_) => return 1,
        Ok(_) => return 0,
    }
    // context goes out of scope, gets dropped safely, all resources released etc
}

fn main() {
    let args_vec: Vec<_> = env::args().collect();

    let result = c_main(&args_vec);

    process::exit(result);
}

@ticki
Copy link
Contributor

ticki commented May 19, 2016

Well, that's fine, although I'd recommend to keep the whole body inside the secondary function, since that way you are less likely to commit logic errors.

@ActualizeInMaterial
Copy link

ActualizeInMaterial commented Jun 23, 2016

If main ends up returning exit codes, let's hope it doesn't do it like this(if this makes sense):

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 only call this function at a known point where there are no more destructors left to run.

src: https://doc.rust-lang.org/std/process/fn.exit.html

EDIT: dumb example but you get the idea: https://play.rust-lang.org/?code=struct%20HasDrop%3B%0A%0Aimpl%20Drop%20for%20HasDrop%20%7B%0A%20%20%20%20fn%20drop(%26mut%20self)%20%7B%0A%20%20%20%20%20%20%20%20println!(%22Dropping!%22)%3B%0A%20%20%20%20%7D%0A%7D%0A%0Afn%20main()%20%7B%0A%20%20%20%20let%20x%20%3D%20HasDrop%3B%0A%0A%20%20%20%20%2F%2F%20do%20stuff%0A%20%20%20std%3A%3Aprocess%3A%3Aexit(2)%3B%0A%7D%20%2F%2F%20x%20goes%20out%20of%20scope%20here%0A&version=stable&backtrace=0

Oh wait, looks like the above comment is a good workaround.

EDIT2: I guess I should've read this whole thread. My bad! buhuhuhu :D I'll kick myself out, :))

@nrc nrc added the T-lang Relevant to the language team, which will review and decide on the RFC. label Aug 29, 2016
@kornelski
Copy link
Contributor

I feel return codes here are a distraction. Don't try to make a return type handle your errors. If you need particular error handling, handle it inside main.

The terse syntax is needed for code examples, and adding complex syntax and handling for it defeats the purpose.

So I think it'd be perfectly fine if main() always panicked when given an Err.

@jaroslaw-weber
Copy link

i dont like the idea to change main signature but maybe it would be nice to have optional alternative.

@mgattozzi
Copy link
Contributor

@jaroslaw-weber you might want to take a look at #1937 which is in it's Final Comment Period.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
T-lang Relevant to the language team, which will review and decide on the RFC.
Projects
None yet
Development

Successfully merging a pull request may close this issue.