Best practice error handling for binary that itself can error, but also checks if another binary has errored?

Hey everyone,

I have a binary that performs a healthcheck on another binary. (it checks if the timestamp is out of date. The other binary promises to periodically write a timestamp, and if the timestamp is ever out of date, something is wrong)

In my healthcheck binary, when this "something is wrong" case is encountered, should I:

  • execute std::process::exit(1)
  • return Ok(Healthcheck::Failed) (where Healthcheck: std::process::Termination (with exit code 1)
    • (and return Ok(Healthcheck::Succeeded) in the happy case (with exit code 0)
  • return Err(MyErrors::HealthcheckFailed) -> (where MyErrors: std::process::Termination)
  • just return Ok(ExitCode)/Err(ExitCode) directly, and println any error message

Also, should i chose negative numbers for when the app itself errors (a healthcheck could not be performed)?

Basically, i'm torn between putting the heatthcheck failed into the Ok or Err branch:

enum Healthcheck {
    Succeeded,
    Failed,
}
impl Termination for Healthcheck {
    fn report(self) -> ExitCode {
        match self {
            Healthcheck::Succeeded => ExitCode::from(0),
            Healthcheck::Failed => ExitCode::from(1),
        }
    }
}

#[derive(Debug, Error)]
enum HealthcheckError {
    #[error("deadline ({0}) exceeded")]
    Failed(DateTime<Utc>),
    #[error("healthcheck path not configured")]
    NoPath,
    #[error("healthheck not understoond: {0}")]
    ParseError(chrono::format::ParseError),
    #[error(transparent)]
    Io(#[from] std::io::Error),
}
impl Termination for HealthcheckError {
    fn report(self) -> ExitCode {
        let code = match self {
            HealthcheckError::Failed(_) => 1,
            HealthcheckError::NoPath => 2,
            HealthcheckError::ParseError(_) => 3,
            HealthcheckError::Io(_) => 4,
        };
        ExitCode::from(code)
    }
}

If I put it in the Err branch, i get nice error message for free. don't have to remember to do a println, etc.

it doesn't work anyway...

     Running `target/debug/healthcheck`
Error: ParseError(ParseError(TooLong))

cabinet2/backend on  master [$!] via 🦀 v1.73.0 
❯ echo $?
1

If I understand your question correctly, I feel like conceptually returning Err(HealthCheckError) is more sensible than Ok(HealthCheck::Failed). Putting something that indicates failure (in your case the heath check) in the Ok branch feels wrong to me.

Right, i'm just wondering if it's good to distinguish between, "I did everything right, but the thing im checking is in an errored state" and "I couldn't even check because i'm in an errored state"

This is how to actually get the desired effect

fn main() -> ExitCode {
    match healthcheck() {
        Ok(()) => ExitCode::from(0),
        Err(e) => {
            eprintln!("{e}");
            e.report()
        }
    }
}
1 Like