Controlled non-verbose exit

Hi!

This question is about terminating a Rust process cleanly, printing only my own error message, without tracing information.

I have a function, cli(), in a module. It has this signature:

pub fn cli() -> Result<CliOpt, CliError>

CliError is defined like this:

use thiserror::Error;

#[derive(Debug, Error)]
#[non_exhaustive]
pub enum CliError {
    #[error("Missing argument(s).")]
    MissArgument,
    ...
}

The cli() function returns early as soon as it detects an unrecoverable error, so there are several statements like this in it:

if x < 5 {
    return Err(CliError::MissArgument);
}

and if successful, it finally returns a result which is a CliOpt struct.

Ok(result)

This seems to work fine. But how can I in the simplest possible way terminate the program from main(), when cli() fails, so that I only print out the error message to stderr?

I currently use panic!() like this:

let urgs = cli();
match urgs {
    Ok(ref r) => { println!("All is well"); dbg!(&r);},
    Err(ref e) => panic!("All is bad: {}", e),
}

On error, the output is

thread 'main' panicked at 'All is bad: Missing argument(s).', src/main.rs:51:23
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

The actual error message drowns in debugging output which isn't user friendly.

How do I restrict the output to only read
All is bad: Missing argument(s).

If this was C or C++, I would simple call exit(), but Rust manuals and tutorials don't recommend it, since it doesn't terminate the process cleanly.

You can return a Result<(), E> from the main function where E implements Debug. libstd will then print the error and produce exit code libc::EXIT_FAILURE. See the implementations of Termination to see what you can return from main.

Is there any reason you can't do this?

eprintln!("All is bad: {e}");
std::process::abort();

The documentation says this about abort()

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.

Rust IO buffers (eg, from BufWriter) will not be flushed.

So abort() and exit() seem to have the same problems, they kill the process without giving it a chance to clean up.

If you don't have any destructors that you care about, then it doesn't matter. You can call abort just fine.

In this particular case, I don't have need for destructors, but I'm new to Rust so I want to learn how to do it correctly and in general.

I mean, if your main function consists of calling the "function that returns the result", and then displaying that result, there's not really anything else to destruct. Maybe flushing stdout before terminating.

If you don't care about setting the exit code, you can return from main normally, but insofar as I know there's no way to set the exit code without either terminating the process or having some extra output generated.

I have changed the code like this:

fn main() -> Result<(), product::CliError> {
    ...
    match urgs {
        Ok(ref r) => { println!("All is well"); dbg!(&r);},
        Err(e) => return Err(e),
    }
    ...
    Ok(())
}

The output is now
Error: MissArgument

So I get the error enum, not the actual message.
It is at least shorter, but not perfect.

When you want to customize how errors look, people often do this:

fn main() {
    match real_main() {
        0 => return,
        ret => std::process::exit(ret),
    }
}

fn real_main() -> i32 {
    ...
}

You can print your error message from real_main, and then you return the error code you want.

Since main has no variables with destructors, this avoids any problems with the exit function.

2 Likes

One easy way to return from main with a not-too-badly-formatted error could be the anyhow crate and to use the question mark operator.

fn main() -> Result<(), anyhow::Error> {
    let _i: i32 = "fifteen".parse()?;
    Ok(())
}

(Playground)

You could extend that to this:


fn main() {
    match error_handler() {
        0 => return,
        ret => std::process::exit(ret),
    }
}

fn error_handler() -> i32 {
    match real_main() {
        Ok(()) => 0,
        Err(MyErrorType::MissingArgument { arg }) => {
            eprintln!("Argument {arg} is missing.");
            1
        }
        Err(MyErrorType::ArgumentNotInteger { value }) => {
            eprintln!("Got '{value}', but it should be an integer.");
            1
        }
    }
}

fn real_main() -> Result<(), MyErrorType> {
    todo!()
}

enum MyErrorType {
    MissingArgument {
        arg: &'static str,
    },
    ArgumentNotInteger {
        value: String,
    },
}

I agree, I don't think its possible to set the exit code before termination. Even if you tried I guess the OS would overwrite the value anyway.

But yes, I forgot to mention, I want to print a final error message to stderr and set an error code (to be captured as $? in Linux/UNIX).

The reason I want Rust to clean up is that I want to ensure everything is properly closed and terminated. Let's say I have an old time modem that dials out through a very expensive telephone line, and I forget to kill the connection, and exit() doesn't do it for me. That could be very expensive.

This seems to work. It doesn't use the error messages defined in my thiserror enum, but I can live with that.

Thanks for the link to anyhow. It holds a useful comparison to other alternatives, and thiserror is one of them.

I do the following:

  1. Implement the Termination trait on a struct and return it from main. (This allows me to control the output on error and process return codes).
  2. Create a main2() that is called from main(). (main() is very minimal -- it's primary role is just to call main2()).

The main()/main2() split serves two roles:

  1. I make sure that no calls in main2() will trigger an immediate process termination (via std::process:exit()), so all drop handlers in main2() are run.
  2. If I want to return early (due to errors) from main(), I can't use the ? operator, because there's no support for it on objects that implement Termination. I can do explicit conversion, but it's annoying to have to do it in multiple places. The split allows me to put it in only one place (in main(), where main2() is called).

It is possible to get ? support for objects that implement Termination on nightly though (using try_trait_v2).

I think writing cli programs in Rust with regards to these specific issues are not super pretty (though to be fair, it's generally a very small part of a clip application). It got better when the Termination trait was stabilized, but it would be much nicer if we had "try v2" as well.

I'll test this also (after a break). I guess it's similar to the solution @alice suggested.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.