I published my first crate: lazy_errors (handle/return groups of errors easily)

65 points lintermute

Hey there!

I'd like to announce that I published my first Rust crate a few weeks ago: lazy_errors. The main purpose of this crate is to allow you to defer handling individual Result::Errs and to group and nest arbitrary error types ergonomically, enabling you to return multiple error values from functions at the same time. Here's a link: https://crates.io/crates/lazy\_errors

I started learning Rust five years ago in my spare time. There is so much I love about the community and the ecosystem: The language is both nice to read and write, very expressive, and the community has documented extensively how to design good, idiomatic solutions. There are a lot of powerful crates that just feel natural. For example, thiserror and anyhow/eyre are just so fundamental to defining or handling errors ergonomically, I've been using them in all of my projects.

Recently however, I wanted to return one or two errors at the same time from a function, depending on what went wrong. I did a little research and found that currently, the most idiomatic solution to return an arbitrary number of errors from functions apparently seems to be to return Result<_, Vec<SomeErrorType>> and/or use iter.partition(Result::is_ok). But doing so may require boilerplate in some cases and Vec<SomeErrorType> makes you miss anyhow/eyre ergonomics. I also found several crates that address this issue. However, to be honest, I wanted to explore a few different approaches myself, kind of as a fun Rust learning challenge. Finally I've settled with the approach I published in the lazy_errors crate. Please judge for yourself whether this is idiomatic or not. Here's a minimal example:

use core::str::FromStr;

use lazy_errors::{prelude::*, Result};

fn run() -> Result<()>
{
    let mut errs = ErrorStash::new(|| "There were one or more errors");

    u8::from_str("42").or_stash(&mut errs);   // `errs` contains 0 errors
    u8::from_str("❌").or_stash(&mut errs);   // `errs` contains 1 error
    u8::from_str("1337").or_stash(&mut errs); // `errs` contains 2 errors

    cleanup().or_stash(&mut errs); // Run cleanup even if there were errors,
                                   // but cleanup is allowed to fail as well

    errs.into() // `Ok(())` if `errs` is still empty, `Err` otherwise
}

fn main()
{
    let err = run().unwrap_err();

    let n = err.childs().len();
    eprintln!("Got {n} error(s).");
    eprintln!("---------------------------------------------------------");
    eprintln!("{err:#}");
}

Running the example will print:

Got 2 error(s).
---------------------------------------------------------
There were one or more errors
- invalid digit found in string
  at src/main.rs:10:24
- number too large to fit in target type
  at src/main.rs:11:26

Basically, ErrorStash is more or less a "builder" for Result<(), E>, where each E basically is a Vec<E>. In other words, you're not even limited to returning a list of errors, you can return an entire error tree. Also, there are blanket implementations on Result to "stash" a wide variety of error types, both in no_std mode as well as when using std::error::Error as base.

There's more you can do with lazy_errors:

  • You can treat individual Errs as critical while still returning all prior non-critical errors at the same time, using the try2! macro (will probably be replaced by the ? operator once the Try trait is stable).
  • You can place a try2!(error_stash.ok()) at some point to ensure that no errors have occurred so far at all.
  • You can create ad-hoc errors from any (format) string using the err! macro.
  • anyhow/eyre-like wrapping of arbitrary errors with an additional message is supported as well (or_wrap_with).
  • If you don't use the prelude, lazy_errors can support arbitrary error types even if they don't implement common traits such as std::error::Error/Display/Debug/Send/Sync.

You can find a more comprehensive example at the top of the docs: https://docs.rs/lazy\_errors/latest/lazy\_errors/

While lazy_errors works standalone, it's not intended to replace anyhow or eyre. I started this project to explore approaches for returning multiple errors simultaneously.

I'd really like to hear your feedback! What do you think about that approach and the lazy_errors crate?