Collecting as many errors as possible before deciding to start the main program logic, how?

I never liked program that show 1 error, you fix it you run it again, it shows another error and so on. For my programs i like to collect as many errors as possible and show them all. I have the following program:

extern crate clap;
extern crate yansi;
extern crate serde_json;

use clap::{Arg, App};
use std::fs::File;
use yansi::Paint;
use serde_json::{Value, Error};

fn main() {
    // https://github.com/clap-rs/clap#quick-example
    let matches = App::new("json")
        .arg(Arg::with_name("first file")
            .index(1)
            .required(true))
        .arg(Arg::with_name("second file")
            .index(2)
            .required(true))
        .get_matches();

    let fp_a = File::open(matches.value_of("first file").unwrap());
    let fp_b = File::open(matches.value_of("second file").unwrap());

    // https://stackoverflow.com/a/27590832
    // https://docs.rs/yansi/0.4.0/yansi/
    if fp_a.is_err() {
      eprintln!("{}", Paint::red("First file could not be found or opened."));
    }

    if fp_b.is_err() {
      eprintln!("{}", Paint::red("Second file could not be found or opened."));
    }

    if fp_a.is_ok() {
      let v_a: Result<Value, Error> = serde_json::from_reader(fp_a.as_ref().unwrap());
      if v_a.is_err() {
        eprintln!("{}", Paint::red("First file is not a valid json file."));
      }
    }

    if fp_b.is_ok() {
      let v_b: Result<Value, Error> = serde_json::from_reader(fp_b.as_ref().unwrap());
      if v_b.is_err() {
        eprintln!("{}", Paint::red("Second file is not a valid json file."));
      }
    }

    if fp_a.is_err() || fp_b.is_err() || v_a.is_err() || v_b.is_err() {
      eprintln!("{}", Paint::red("Aborting due to previous errors."));
    }

    println!("Rest of program with valid input ..");
}

The problem is that here fp_a.is_err() || fp_b.is_err() || v_a.is_err() || v_b.is_err() v_a and v_b are not in scope. I tried moving code around in all sorts of different ways, but it creates other problems.

If i only had file A it would have been easy because i can keep nesting if-else blocks. But since i have two files this is not possible. Each file sets in motion a chain of error checks. Only two are shown here: 1. can file be opened 2. is it valid json. But i might add more later. Is there some error collecting mechanism (monad ??) of some sorts? I think the code as it is now is kind of ugly with all the is_err() and is_ok() function calls.

This is a case for Result::and_then().

1 Like

Yes, but it is pretty heavily tied to early returns. (I'm talking about the ? "try" operator.) In the future, we may get try {} blocks which will make it easier to collect errors together like you want.

One thing to consider is using a tuple match:

match (fp_a.is_ok(), fp_b.is_ok()) {
    (true, true) => (),
    (true, false) => (),
    (false, true) => (),
    (false, false) => (),
}
1 Like

Here’s one version that uses a combination of map_err for printing errors as soon as they occur, and then a tuple match as suggested above for checking whether any errors occurred.

extern crate clap;
extern crate serde_json;

use clap::{Arg, App};
use std::fs::File;
use serde_json::Value;

fn read(path: &str, ctx: &str) -> Result<Value, ()> {
    let f = File::open(path)
        .map_err(|_| eprintln!("{} could not be found or opened", ctx))?;
    serde_json::from_reader(f)
        .map_err(|_| eprintln!("{} is not a valid json file", ctx))
}

fn main() {
    // https://github.com/clap-rs/clap#quick-example
    let matches = App::new("jsondiff")
        .arg(Arg::with_name("first file").index(1).required(true))
        .arg(Arg::with_name("second file").index(2).required(true))
        .get_matches();

    let a = read(matches.value_of("first file").unwrap(), "First file");
    let b = read(matches.value_of("second file").unwrap(), "Second file");

    let (v_a, v_b) = match (a, b) {
        (Ok(a), Ok(b)) => (a, b),
        _ => panic!("{}", "Aborting due to previous errors.")
    };

    println!("Hello, world!");
}
1 Like

You can take advantage of the fact that errors are regular objects, which you can put in a vec and process in a loop.

results.push(foo());
results.push(bar());

let failures = results.iter().filter(|res| res.is_err()).peekable();

if failures.peek().is_some() {
    eprintln!("Here's stuff that failed:"):
    for fail in failures {
       eprintln!("{}", fail);
    }
    exit(1);
}

You can even make functions return Result<(), Vec<Error>>.

3 Likes