Accumulating multiple errors (error products)

I've hit a situation where I want to do multiple operations (in this case, save several files), and if any number of them fail, I want to report all failures, not just the first.

// not this
fn bad() -> Result<(), SaveError> {
  save_1()?;
  save_2()?;
  Ok(())
}

// more like this
fn save_many() -> Result<(),SeveralSaveError> {
  let res1 = save_1();
  let res2 = save_2();
  let errs : Vec<_> = [res1,res2].into_iter().filter_map(|r|r.err()).collect();
  if !errs.is_empty() {
    Err(errs)?
  }
  Ok(())
}
enum SaveError {
  AnyFailed(Vec<IoError>),
}

This is fairly cumbersome, though, and this seems like a problem that there could already be a purpose-built crate for, that I just haven't found yet. Does anyone know existing crates for this? Or a nice idiom for how to achieve this without a library? (googling "rust multiple errors" doesn't give many useful results)

So far, I've found frunk::validated. Are there others?

3 Likes

Collating multiple errors into one is not well supported. The std error trait only supports linear error stacks (via Error::source) and most error handling libraries follow suit.

Why? Primarily because it's not easy to have a dyn-safe trait provide an iterator of multiple sources; you're limited to internal iteration (e.g. for_each_cause(&self, impl FnMut(&dyn MyError)), boxing (e.g. fn causes(&self) -> Box<dyn Iterator<Item=&dyn MyError>>), or homogeneous slices (e.g. fn sources(&self) -> &[io::Error]). And error traits are generally expecting to end up being put into a dyn trait object eventually.

That said, both miette and error-stack do have some support for "tree errors" with multiple causes. There doesn't seem to be any real ?/monadish support for collecting multiple errors, though, likely in part due to the limited availability of "tree error" reporting.

1 Like

Ironically, rustc itself is a prime example of a program that collates multiple errors rather than bailing out on the first one!

2 Likes

And actually an illustration of a slightly harder organizational problem because rustc isn't fn(Source) -> Result<Program, Vec<Diagnostic>>, it's more fn(Source) -> (Result<Program, ()>, Vec<Diagnostic>), which can be a more awkward shape to deal with.

The best I've personally found for small cases is something roughly in the shape of

struct FatalError;
impl From<()> for FatalError {
    fn from(_: ()) -> Self { Self }
}

type Saves = [Save; 2];
type SaveMany = Result<Saves, FatalError>;

fn save_many_with(emit: &mut dyn FnMut(Diagnostic)) -> SaveMany {
    let save1 = save1().map_err(&mut *emit);
    let save2 = save2().map_err(&mut *emit);
    Ok([save1?, save2?])
}

// for warning diagnostic collection
fn save_many() -> (SaveMany, Vec<Diagnostic>>) {
    let mut diags = vec![];
    let emit = &mut |diag| diags.push(diag);
    let saves = save_many_with(emit);
    (saves, diags)
}

// for tree errors
fn save_many() -> Result<Saves, Vec<Diagnostic>> {
    let mut diags = vec![];
    let emit = &mut |diag| diags.push(diag);
    let saves = save_many_with(emit);
    saves.map_err(|FatalError| diags)
}

Basically, the pattern is to have some "error sink" which you can map errors into, deal only in unit errors otherwise, then ? the emitted-error-results only when the lack of result can no longer be tolerated. This works irregardless of whether the top layer is Result<T, Es> or (Option<T>, Es).

This currently uses Result<T, ()> so you can ? it in -> Result<T, FatalError>, but you could similarly use Option<T> everywhere, or potentially once Try is stable, enable FromResidual<Option<!>> for Result<T, FatalError>.

3 Likes

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.