dtolnay / anyhow

Flexible concrete Error type built on std::error::Error
Apache License 2.0
5.49k stars 147 forks source link

Recommendation on mixing exit codes with anyhow #247

Open chipsenkbeil opened 2 years ago

chipsenkbeil commented 2 years ago

With Rust 1.61.0 out, the Termination trait and associated ExitCode are now stable. In the past, I maintained a large error enum purely to decipher what error code to return, printed out the error using eprintln!, and then exited with std::process::exit.

Now, I'm thinking through a way for me to switch over to anyhow because it would streamline so much in my application, but I still need some way to keep track of what exit code to return for different errors. Looking for any advice/thoughts on how I could capture/encode that information when bubbling up errors.

Implement a wrapper type for anyhow::Error at the end

One way would be to create a newtype wrapper for the error and then use downcasting to figure out the underlying error with an associated exit code.

use std::process::{ExitCode, Termination};

struct AppResult(anyhow::Result<()>);

impl Termination for AppResult {
    fn report(self) -> ExitCode {
        match self {
            Ok(_) => ExitCode::SUCCESS,
            Err(x) => {
                if self.downcast_ref::<MyErrorType1>().is_some() {
                    ExitCode::from(11)
                } else if self.downcast_ref::<MyErrorType2>().is_some() {
                    ExitCode::from(22)
                } else {
                    ExitCode::FAILURE
                }
            }
        }
    }
}

fn main() -> AppResult {
    AppResult(real_main())
}

fn real_main() -> anyhow::Result<()> {
    // ...
}

Derive an error type with an exit code

use std::process::{ExitCode, Termination};

// Assume that this type implements:
// 1. Display that yields the context
// 2. std::error::Error
struct ExitCodeError {
    error: Box<dyn std::error::Error + Send + Sync>, 
    exit_code: ExitCode,
}

struct AppResult(anyhow::Result<()>);

impl Termination for AppResult {
    fn report(self) -> ExitCode {
        match self {
            Ok(_) => ExitCode::SUCCESS,
            Err(x) => {
                if let Some(x) = self.downcast::<ExitCodeError>() {
                    x.exit_code
                } else {
                    ExitCode::FAILURE
                }
            }
        }
    }
}

fn main() -> AppResult {
    AppResult(real_main())
}

fn real_main() -> anyhow::Result<()> {
    // ...

    // For each error, we have to wrap the error in our ExitCodeError first if we want a unique exit code
    // This seems really verbose still, so maybe there's a way to simplify
    let value = do_something().map_err(|error| ExitCodeError { 
        error: Box::new(error), 
        exit_code: ExitCode::from(22) 
    })?;

    // ...
}

Some cleaner way?

Ideally, I'd love something like

use std::process::ExitCode;

fn main() -> anyhow::Result<()> {
    std::fs::read("some/path")
        .exit_context(ExitCode::from(22), "Failed with specific context")?;
    // ...
}
ofek commented 1 year ago

@dtolnay What would you recommend?

benaryorg commented 1 year ago

To bring in a specific use case; some (most?) crond implementations support the use of EAGAIN as an exit code to trigger a retry. I am currently running into an issue with software where it would be incredibly useful to tag certain error types created with thiserror as being transient errors which can be retried and have the others be permanent errors. A similar thing could be achieved by using Result<Termination> as in the example provided by OP where I have another layer of main and wrap handle this in an elaborate match statement, but having this integrated in either anyhow would be nice, and it would be even better if it was implemented in a way that thiserror could integrate with.