Error handling near the top level of a command line tool

I'm writing some command line tools and I find that the ? operator cannot help me in the outermost layers of the code, which wind up looking very much like how I would have written the same thing in C, for example

struct Args {
    input: PathBuf,
    output: PathBuf,
    // ...
}

fn run(args: Args) -> i32 {
    let input = match fs::open(&args.input) {
        Ok(d) => d,
        Err(e) => {
            eprintln!("{}: {}", args.input.display(), e);
            return 1;
        }
    };
    let mut output = match fs::create(&args.output) {
        Ok(d) => d,
        Err(e) => {
            eprintln!("{}: could not create: {}", args.output.display(), e);
            return 1;
        }
    };
    // ... actual work here ...
    if let Err(e) = output.flush() {
        eprintln!("{}: write error: {}", args.output.display(), e);
        return 1;
    }
    0
}

fn main() {
    std::process::exit(run(parse_args()));
}

Is there a preferred, or at least less repetitive, way to write this kind of code? Note that control over the details of the error messages is very important to me, which is why I'm not having main return io::Result<()>.

I guess what I would recommend trying instead is splitting main into 2 parts, one returning a custom error with your specific error formatting or the same using thiserror (or both and then cargo expand to see if they are basically the same, and whether thiserror is worth it to you).

I haven't tried to compile the following, but it is basically along the lines of where I would start...

use thiserror::Error;
#[derive(Error)]
enum MyError {
   #[error("custom formatting {0}")]
   Io(#[from] io::Result);
   #[error("some other formatting {0}")]
   SomeSpecificFormatting(#[from] io::Result);

}

fn main() -> i32 {
    if let Err(e) = inner_main() {
      eprintln!("custom error output {}", e.to_string());
      code
   } else {
      0
   }
}

fn inner_main() -> Result<(), MyError> {
   // If you need different formatting for the same error in multiple ways
   // honestly don't know if you need this.
   Ok(()).map_err(MyError::SomeSpecificIoFormatting)
}
1 Like

Inside run, you can map all errors to a simple string and return them with the question mark operator. You can then match the return value of run in main to print the error and return an exit code.

(I changed your file api to std)

fn run(args: Args) -> Result<(), String> {
    let input = File::open(&args.input).map_err(|e| format!("{}: {}", args.input.display(), e))?;
    let mut output = File::create(&args.output).map_err(|e| format!("{}: could not create: {}", args.output.display(), e))?;
    // ... actual work here ...
    output.sync_all().map_err(|e| format!("{}: write error: {}", args.output.display(), e))?;
    Ok(())
}

fn main() -> ExitCode {
    match run(parse_args()) {
        Ok(()) => ExitCode::from(0),
        Err(s) => {
            eprintln!("{}", s);
            ExitCode::from(1)
        }
    }
}
2 Likes

You could use the newly stabalized inspect_err to print and ExitCode to return termination status.

fn run(args: Args) -> Result<(), Error> {
    let input = match fs::open(&args.input)
        .inspect_err(|e| eprintln!("{}: {}", args.input.display(), e))?;
    let mut output = match fs::create(&args.output)
        .inspect_err(|e| eprintln!("{}: could not create: {}", args.output.display(), e))?;
    output.flush()
        .inspect_err(|e| eprintln!("{}: write error: {}", args.output.display(), e))?;
    Ok(())
}

fn main() -> ExitCode {
    match run(parse_args()) {
       Ok(_) => ExitCode::SUCCESS,
       Err(_) => ExitCode::FAILURE,
    }
}

I'm not sure why it didn't occur to me that the Err side of a Result could be a bare String. I'll have to experiment a little but that might just work.

In case you want to communicate more, like a specific exit code or something, you can also return more complicated types like Result<(), (String, u8)>. But at that point also consider making your own Error enum for this purpose, with a variant for each possible error and data associated with it. You can put all the error messages in its Display impl.

2 Likes