#error #error-handling #multiple #tree

no-std lazy_errors

轻松创建、分组和嵌套任意错误,并优雅地延迟错误处理

8个版本 (破坏性更新)

0.7.0 2024年7月9日
0.6.0 2024年6月25日
0.5.0 2024年6月11日
0.4.0 2024年6月8日
0.1.1 2024年5月24日

432Rust模式

Download history 40/week @ 2024-05-17 154/week @ 2024-05-24 2/week @ 2024-05-31 504/week @ 2024-06-07 32/week @ 2024-06-14 148/week @ 2024-06-21 11/week @ 2024-06-28 101/week @ 2024-07-05 24/week @ 2024-07-12 3/week @ 2024-07-19 16/week @ 2024-07-26 4/week @ 2024-08-02

每月下载量:57

MIT/Apache

150KB
1.5K SLoC

lazy_errors 许可:MIT OR Apache-2.0 lazy_errors on crates.io lazy_errors on docs.rs 源代码仓库

轻松创建、分组和嵌套任意错误,并优雅地延迟错误处理。

#[cfg(feature = "std")]
use lazy_errors::{prelude::*, Result};

#[cfg(not(feature = "std"))]
use lazy_errors::surrogate_error_trait::{prelude::*, Result};

fn run(input1: &str, input2: &str) -> 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

    // `input1` is very important in this example,
    // so make sure it has a nice message.
    let r: Result<u8> = u8::from_str(input1)
        .or_wrap_with(|| format!("Input '{input1}' is invalid"));

    // If `input1` is invalid, we don't want to continue
    // but return _all_ errors that have occurred so far.
    let input1: u8 = try2!(r.or_stash(&mut errs));
    println!("input1 = {input1:#X}");

    // Continue handling other `Result`s.
    u8::from_str(input2).or_stash(&mut errs);

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

fn main()
{
    let err = run("", "").unwrap_err();
    let n = err.children().len();
    eprintln!("Got an error with {n} children.");
    eprintln!("---------------------------------------------------------");
    eprintln!("{err:#}");
}

运行示例将打印

Got an error with 3 children.
---------------------------------------------------------
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
- Input '❓' is invalid: invalid digit found in string
  at src/main.rs:16:10
  at src/main.rs:20:30

概要

lazy_errors 提供了类型、特性和对 Result 的泛型实现,可以用于优雅地延迟错误处理。 lazy_errors 允许你轻松创建临时错误以及将各种错误封装在单个通用错误类型中,简化了你的代码库。在这方面,它与 anyhow/eyre 类似,但其报告方式不够花哨或详细(例如,lazy_errors 跟踪源代码文件名和行号,而不是提供完整的 std::backtrace 支持)。另一方面,lazy_errorsResult 添加了方法,让你在失败时继续执行,延迟返回 Err 结果。 lazy_error 还支持嵌套错误。当你从函数返回嵌套错误时,错误将形成一个树状结构并在“冒泡”。你可以完整地向用户/开发者报告该错误树。

默认情况下,lazy_errors 将将错误值装箱(如 anyhow/eyre),这允许你在相同的 Result 类型中使用不同的错误类型。然而,如果你明确提供了静态错误类型信息,lazy_errors 将会尊重它。如果你这样做,你可以在运行时访问错误值的字段和方法,而无需进行向下转换。这两种操作模式可以一起工作,如下面的示例所示。

虽然默认情况下 lazy_error 通过 std::error::Error 集成,但它也支持 #![no_std],如果你禁用了 std 功能。当你定义一些简单的类型别名时,lazy_errors 可以轻松支持那些不是 Sync 或甚至 Send 的错误类型。

使用此包的常见原因包括

  • 你想要返回一个错误,但在返回之前运行一些可能失败的清理逻辑。
  • 更普遍地,你正在调用两个或多个返回 Result 的函数,并希望返回一个包含所有已发生错误的错误。
  • 你正在启动几个并行活动,等待它们的完成,并希望返回所有发生的错误。
  • 在运行一些报告或恢复逻辑之前,你想要聚合多个错误,遍历收集到的所有错误。
  • 你需要处理不实现 std::error::Error/Display/Debug/Send/Sync 或其他常见特质的错误。

特性标志

  • std:
    • 支持实现 std::error::Error 的错误类型。
    • lazy_error 错误类型实现 std::error::Error
  • eyre:添加 into_eyre_resultinto_eyre_report 转换。
  • rust-vN(其中 N 是 Rust 版本号):仅添加了对在相应 Rust 版本中稳定的 corealloc 中的某些错误类型的支持。

MSRV

lazy_errors 的 MSRV 依赖于启用的功能集

  • Rust 1.77 支持所有功能及其所有组合。
  • Rust 版本 1.61 .. 1.77 需要你禁用所有 rust-vN 功能,其中 N 大于你的 Rust 工具链版本。例如,要在 Rust 1.66 上编译 lazy_errors,你必须禁用 rust-v1.77rust-v1.69,但不是 rust-v1.66
  • eyre 至少需要 Rust 1.65。
  • Rust 版本低于 1.61 不受支持。

教程

lazy_errors 实际上可以支持任何错误类型,只要它是 Sized;它甚至不需要是 SendSync。你只需要相应地指定泛型类型参数,如下页底部的示例所示。通常,你想要使用来自 prelude 的别名。当你使用这些别名时,错误将被装箱,你可以从同一函数动态返回不同类型的错误组。

默认情况下启用了 std 功能,使得 lazy_error 能够支持实现 std::error::Error 的第三方错误类型。在这种情况下,从这个crate中所有的错误类型都将实现 std::error::Error。如果您需要 #![no_std] 支持,您可以禁用 std 功能并使用 surrogate_error_trait::prelude 代替。如果您这样做,lazy_errors 将将任何实现 surrogate_error_trait::Reportable 标记特质的错误类型进行装箱。如果需要,您也可以为您的自定义类型实现该特质(只需一行代码)。

lazy_errors 可以独立工作,但它并不打算取代 anyhoweyre。相反,这个项目开始是为了探索如何运行多个可能出错的操作,聚合它们(如果有)的错误,并通过从返回 Result 的函数中返回所有这些错误来延迟实际的错误处理/报告。通常,Result<_, Vec<_>> 可以用于此目的,这与 lazy_errors 内部所做的并没有太大的区别。然而,lazy_errors 提供了“语法糖”,使这种方法更加方便。因此,可以说这个crate中最有用的方法是 or_stash

示例: or_stash

or_stash 可以说是这个crate中最有用的方法。一旦您导入了 OrStash 特质或 prelude,它就会在 Result 上可用。以下是一个示例

#[cfg(feature = "std")]
use lazy_errors::{prelude::*, Result};

#[cfg(not(feature = "std"))]
use lazy_errors::surrogate_error_trait::{prelude::*, Result};

fn run() -> Result<()>
{
    let mut stash = ErrorStash::new(|| "Failed to run application");

    print_if_ascii("").or_stash(&mut stash);
    print_if_ascii("").or_stash(&mut stash);
    print_if_ascii("42").or_stash(&mut stash);

    cleanup().or_stash(&mut stash); // Runs regardless of earlier errors

    stash.into() // `Ok(())` if the stash was still empty
}

fn print_if_ascii(text: &str) -> Result<()>
{
    if !text.is_ascii() {
        return Err(err!("Input is not ASCII: '{text}'"));
    }

    println!("{text}");
    Ok(())
}

fn cleanup() -> Result<()>
{
    Err(err!("Cleanup failed"))
}

fn main()
{
    let err = run().unwrap_err();
    let printed = format!("{err:#}");
    let printed = replace_line_numbers(&printed);
    assert_eq!(printed, indoc::indoc! {"
        Failed to run application
        - Input is not ASCII: '❓'
          at src/lib.rs:1234:56
          at src/lib.rs:1234:56
        - Input is not ASCII: '❗'
          at src/lib.rs:1234:56
          at src/lib.rs:1234:56
        - Cleanup failed
          at src/lib.rs:1234:56
          at src/lib.rs:1234:56"});
}

在上面的示例中,run() 将打印 42,运行 cleanup(),然后返回存储的错误。

请注意,在上面的例子中,ErrorStash 是手动创建的。在添加第一个错误之前,ErrorStash 是空的。将空的 ErrorStash 转换为 Result 将产生 Ok(())。当在 Result::Err(e) 上调用 or_stash 时,e 将被移动到 ErrorStash 中。一旦在 ErrorStash 中存储了至少一个错误,将 ErrorStash 转换为 Result 将产生一个包含 ErrorResult::Err,这是该包的主要错误类型。

示例:or_create_stash

有时候,你不想提前创建一个空的ErrorStash。在这种情况下,你可以在Result上调用or_create_stash来按需创建一个非空容器, whenever necessary。当在Result::Err上调用or_create_stash时,错误将放入一个StashWithErrors,而不是一个ErrorStashErrorStashStashWithErrors的行为类似。虽然两者都可以接受额外的错误,但StashWithErrors保证是非空的。类型系统会意识到至少有一个错误。因此,虽然ErrorStash只能转换为Result,产生Ok(())Err(e)(其中eError),但这种区分允许直接将StashWithErrors转换为Error

#[cfg(feature = "std")]
use lazy_errors::{prelude::*, Result};

#[cfg(not(feature = "std"))]
use lazy_errors::surrogate_error_trait::{prelude::*, Result};

fn run() -> Result<()>
{
    match write("").or_create_stash(|| "Failed to run application") {
        Ok(()) => Ok(()),
        Err(mut stash) => {
            cleanup().or_stash(&mut stash);
            Err(stash.into())
        },
    }
}

fn write(text: &str) -> Result<()>
{
    if !text.is_ascii() {
        return Err(err!("Input is not ASCII: '{text}'"));
    }
    Ok(())
}

fn cleanup() -> Result<()>
{
    Err(err!("Cleanup failed"))
}

fn main()
{
    let err = run().unwrap_err();
    let printed = format!("{err:#}");
    let printed = replace_line_numbers(&printed);
    assert_eq!(printed, indoc::indoc! {"
        Failed to run application
        - Input is not ASCII: '❌'
          at src/lib.rs:1234:56
          at src/lib.rs:1234:56
        - Cleanup failed
          at src/lib.rs:1234:56
          at src/lib.rs:1234:56"});
}

示例:into_eyre_*

ErrorStashStashWithErrors可以分别转换为ResultError。对于eyre::Resulteyre::Error(即eyre::Report),存在从ErrorStashStashWithErrors到它们的类似但损失性的转换,分别是into_eyre_resultinto_eyre_report

use lazy_errors::prelude::*;
use eyre::bail;

fn run() -> Result<(), eyre::Report>
{
    let r = write("").or_create_stash::<Stashable>(|| "Failed to run");
    match r {
        Ok(()) => Ok(()),
        Err(mut stash) => {
            cleanup().or_stash(&mut stash);
            bail!(stash.into_eyre_report());
        },
    }
}

fn write(text: &str) -> Result<(), Error>
{
    if !text.is_ascii() {
        return Err(err!("Input is not ASCII: '{text}'"));
    }
    Ok(())
}

fn cleanup() -> Result<(), Error>
{
    Err(err!("Cleanup failed"))
}

fn main()
{
    let err = run().unwrap_err();
    let printed = format!("{err:#}");
    let printed = replace_line_numbers(&printed);
    assert_eq!(printed, indoc::indoc! {"
        Failed to run
        - Input is not ASCII: '❌'
          at src/lib.rs:1234:56
          at src/lib.rs:1234:56
        - Cleanup failed
          at src/lib.rs:1234:56
          at src/lib.rs:1234:56"});
}

示例:层次结构

你可能已经注意到,错误Error形成层次结构

#[cfg(feature = "std")]
use lazy_errors::{prelude::*, Result};

#[cfg(not(feature = "std"))]
use lazy_errors::surrogate_error_trait::{prelude::*, Result};

fn parent() -> Result<()>
{
    let mut stash = ErrorStash::new(|| "In parent(): child() failed");
    stash.push(child().unwrap_err());
    stash.into()
}

fn child() -> Result<()>
{
    let mut stash = ErrorStash::new(|| "In child(): There were errors");
    stash.push("First error");
    stash.push("Second error");
    stash.into()
}

fn main()
{
    let err = parent().unwrap_err();
    let printed = format!("{err:#}");
    let printed = replace_line_numbers(&printed);
    assert_eq!(printed, indoc::indoc! {"
        In parent(): child() failed
        - In child(): There were errors
          - First error
            at src/lib.rs:1234:56
          - Second error
            at src/lib.rs:1234:56
          at src/lib.rs:1234:56"});
}

上述示例可能看起来很笨拙。实际上,这个示例只是为了说明错误层次结构。在实际应用中,您不会编写这样的代码。相反,您可能会依赖 or_wrapor_wrap_with

示例:包装

您可以使用 or_wrapor_wrap_with 将任何可以转换为 内嵌错误类型Error 的值进行包装,或者为错误附加一些上下文。

#[cfg(feature = "std")]
use lazy_errors::{prelude::*, Result};

#[cfg(not(feature = "std"))]
use lazy_errors::surrogate_error_trait::{prelude::*, Result};

fn run(s: &str) -> Result<u32>
{
    parse(s).or_wrap_with(|| format!("Not an u32: '{s}'"))
}

fn parse(s: &str) -> Result<u32>
{
    let r: Result<u32, core::num::ParseIntError> = s.parse();

    // Wrap the error type “silently”:
    // No additional message, just file location and wrapped error type.
    r.or_wrap()
}

fn main()
{
    let err = run("").unwrap_err();
    let printed = format!("{err:#}");
    let printed = replace_line_numbers(&printed);
    assert_eq!(printed, indoc::indoc! {"
        Not an u32: '❌': invalid digit found in string
        at src/lib.rs:1234:56
        at src/lib.rs:1234:56"});
}

示例:临时错误

err! 宏允许您格式化字符串,并将其转换为同时是一个临时 Error

#[cfg(feature = "std")]
use lazy_errors::prelude::*;

#[cfg(not(feature = "std"))]
use lazy_errors::surrogate_error_trait::prelude::*;

let pid = 42;
let err: Error = err!("Error in process {pid}");

您通常会找到临时错误是错误树中的叶子。然而,错误树可以具有几乎任何 内嵌错误类型 作为叶子。

支持的错误类型

prelude 模块导出常用的特性和 别名 类型。导入 lazy_errors::prelude::* 应该可以满足大多数用例。您可能还需要导入 lazy_errors::Result。在 ![no_std] 模式或当 core::error::Error 不可用时,您可以使用 surrogate_error_trait::prelude 来代替,并使用相应的 lazy_errors::surrogate_error_trait::Result

当您使用预定义中的别名类型时,如果 E 实现了 Into<Stashable>,则这个库应支持任何 Result<_, E>。基本上,Stashable 是一个 Box<dyn E>,其中 E 要么是 std::error::Error,要么是在 #![no_std] 模式下的代理 trait(《surrogate_error_trait::Reportable))。因此,使用预定义中的别名类型,您放入此库定义的任何容器中的任何错误都将被装箱。选择 Into<Box<dyn E>> 特性约束,因为它为广泛的错误类型或“类似错误”类型实现了。满足此约束的类型的示例包括

  • &str
  • String
  • anyhow::Error
  • eyre::Report
  • std::error::Error
  • 此库中所有的错误类型

此库的主要错误类型是 Error。您可以通过调用 or_wrapor_wrap_with 将所有受支持的“类似错误”类型转换为 Error

换句话说,这个包支持多种错误类型。然而,在某些情况下,你可能需要比这更灵活的处理方式。例如,你可能不希望丢失静态错误类型信息,或者你的错误类型不是Sync。通常,只要E实现了Into<I>,这个包就能很好地与任何Result<_, E>一起工作,其中I内层错误类型 of Error。这个包会将其容器中的错误存储为类型I,例如在ErrorStashError中。当你在使用prelude中的类型别名时,I总是Stashable。然而,你并不需要使用Stashable。你可以随意选择用于I的类型。它可以是自定义类型,并且不需要实现任何特质或自动特质,除了Sized。因此,如果预定义的别名不适用于你的目的,你可以手动导入所需的特性和类型,并定义自定义别名,如下一个示例所示。

示例:自定义错误类型

以下是一个复杂的示例,它不使用prelude,而是定义了自己的别名。在示例中,Error<CustomError>ParserErrorStash不对其错误进行装箱。相反,它们将所有错误类型信息以静态形式呈现,这允许你编写恢复逻辑而无需在运行时依赖向下转换。示例还展示了如何使用具有自定义生命周期的自定义错误类型与装箱错误类型(Stashable)一起使用。

use lazy_errors::{err, ErrorStash, OrStash, StashedResult};

#[cfg(feature = "std")]
use lazy_errors::Stashable;

#[cfg(not(feature = "std"))]
use lazy_errors::surrogate_error_trait::Stashable;

#[derive(thiserror::Error, Debug)]
pub enum CustomError<'a>
{
    #[error("Input is empty")]
    EmptyInput,

    #[error("Input '{0}' is not u32")]
    NotU32(&'a str),
}

// Use `CustomError` as inner error type `I` for `ErrorStash`:
type ParserErrorStash<'a, F, M> = ErrorStash<F, M, CustomError<'a>>;

// Allow using `CustomError` as `I` but use `Stashable` by default:
pub type Error<I = Stashable<'static>> = lazy_errors::Error<I>;

fn main()
{
    let err = run(&["42", "0xA", "f", "oobar", "3b"]).unwrap_err();
    eprintln!("{err:#}");
}

fn run<'a>(input: &[&'a str]) -> Result<(), Error<Stashable<'a>>>
{
    let mut errs = ErrorStash::new(|| "Application failed");

    let parser_result = parse(input); // Soft errors
    if let Err(e) = parser_result {
        println!("There were errors.");
        println!("Errors will be returned after showing some suggestions.");
        let recovery_result = handle_parser_errors(&e); // Hard errors
        errs.push(e);
        if let Err(e) = recovery_result {
            errs.push(e);
            return errs.into();
        }
    }

    // ... some related work, such as writing log files ...

    errs.into()
}

fn parse<'a>(input: &[&'a str]) -> Result<(), Error<CustomError<'a>>>
{
    if input.is_empty() {
        return Err(Error::wrap(CustomError::EmptyInput));
    }

    let mut errs = ParserErrorStash::new(|| {
        "Input has correctable or uncorrectable errors"
    });

    println!("Step #1: Starting...");

    let mut parsed = vec![];
    for s in input {
        println!("Step #1: Trying to parse '{s}'");
        // Ignore “soft” errors for now...
        if let StashedResult::Ok(k) = parse_u32(s).or_stash(&mut errs) {
            parsed.push(k);
        }
    }

    println!(
        "Step #1: Done. {} of {} inputs were u32 (decimal or hex): {:?}",
        parsed.len(),
        input.len(),
        parsed
    );

    errs.into() // Return list of all parser errors, if any
}

fn handle_parser_errors(errs: &Error<CustomError>) -> Result<(), Error>
{
    println!("Step #2: Starting...");

    for e in errs.children() {
        match e {
            CustomError::NotU32(input) => guess_hex(input)?,
            other => return Err(err!("Internal error: {other}")),
        };
    }

    println!("Step #2: Done");

    Ok(())
}

fn parse_u32(s: &str) -> Result<u32, CustomError>
{
    s.strip_prefix("0x")
        .map(|hex| u32::from_str_radix(hex, 16))
        .unwrap_or_else(|| u32::from_str(s))
        .map_err(|_| CustomError::NotU32(s))
}

fn guess_hex(s: &str) -> Result<u32, Error>
{
    match u32::from_str_radix(s, 16) {
        Ok(v) => {
            println!("Step #2: '{s}' is not u32. Did you mean '{v:#X}'?");
            Ok(v)
        },
        Err(e) => {
            println!("Step #2: '{s}' is not u32. Aborting program.");
            Err(err!("Unsupported input '{s}': {e}"))
        },
    }
}

运行上面的示例将产生类似以下输出

stdout:
Step #1: Starting...
Step #1: Trying to parse '42'
Step #1: Trying to parse '0xA'
Step #1: Trying to parse 'f'
Step #1: Trying to parse 'oobar'
Step #1: Trying to parse '3b'
Step #1: Done. 2 of 5 inputs were u32 (decimal or hex): [42, 10]
There were errors.
Errors will be returned after showing some suggestions.
Step #2: Starting...
Step #2: 'f' is not u32. Did you mean '0xF'?
Step #2: 'oobar' is not u32. Aborting program.

stderr:
Application failed
- Input has correctable or uncorrectable errors
  - Input 'f' is not u32
    at src/lib.rs:72:52
  - Input 'oobar' is not u32
    at src/lib.rs:72:52
  - Input '3b' is not u32
    at src/lib.rs:72:52
  at src/lib.rs:43:14
- Unsupported input 'oobar': invalid digit found in string
  at src/lib.rs:120:17
  at src/lib.rs:45:18

许可证

根据您的选择,许可协议为以下之一

贡献

除非你明确声明,否则根据Apache-2.0许可证定义的任何有意提交以包含在作品中的贡献,均应如上双许可,无需任何额外的条款或条件。

依赖项