< 返回版块

oosquare 发表于 2025-02-19 21:10

anyerr 是一个动态错误处理库,提供了一个叫做 AnyError 的类型拥有表示错误,与 anyhow 类似但又有所不同,主要是在功能上更加丰富。大家如果喜欢,欢迎来 GitHub 来给个 star。

特性

anyerr 的各种特性可以总结为以下几点:

  • 错误包装:各错误处理库的基本功能。提供了 helper trait 来简化错误包装过程。
  • 错误类型:本 crate 中又称 error kind。AnyError 可以携带 error kind,而 error kind 上可定制的,可以使用 crate 中预定义的,也可以自定义。
  • 上下文信息:AnyError 可以携带除了 error message 和 error kind 以外的额外的上下文信息,一般表示为一系列 key-value pairs。储存上下文信息的数据结构也可以定制,本 crate 提供了具有不同的特性的变种。
  • 调用堆栈捕获:基本功能。如果 RUST_BACKTRACE 和 RUST_LIB_BACKTRACE 的值指定可以捕获堆栈,则必定会捕获。

安装

Cargo.toml 中添加

[dependencies]
anyerr = "0.1.1"

基本使用方法

定制自己的错误类型

上面提到了 AnyError 是可定制的,事实上,AnyErrorAnyError<C, K>C 代表储存上下文的数据结构,K 是要使用的错误类型。

在自己的 crate 中使用 anyerr 时,一般会基于 AnyError<C, K> 来定制出自己的错误类型,然后在整个 crate 内使用。比如:

// Make this module accessible to your whole crate.
mod err {
    use anyerr::AnyError as AnyErrorTemplate;
    use anyerr::context::LiteralKeyStringMapContext; // A predefined context storage

    pub use anyerr::{Intermediate, Overlay}; // Helper traits.
    pub use anyerr::kind::DefaultErrorKind as ErrKind; // A predefined error kind.
    pub use anyerr::Report; // An error reporting utility.

    pub type AnyError = AnyErrorTemplate<LiteralKeyStringMapContext, ErrKind>;
    pub type AnyResult<T> = Result<T, AnyError>;
}

接下来使用的不再是 anyerr::AnyError,而是自己定制的 err::AnyError

错误的构造和使用

以下展示了如何构造一个 AnyError

use err::*;

fn fail() -> AnyResult<()> {
    // Use `AnyError::minimal()` to create a simple [`String`]-based error.
    Err(AnyError::minimal("this function always fails"))
}

fn check_positive(x: i32) -> AnyResult<()> {
    if x > 0 {
        return Ok(());
    }
    // Use `AnyError::quick()` to quickly create an error with an error
    // message and an error kind.
    Err(AnyError::quick(
        "expects `x` to be a positive number",
        ErrKind::ValueValidation
    ))
}

fn try_add_username(
    usernames: &mut Vec<String>,
    new_username: String
) -> AnyResult<usize> {
    let res = usernames.iter()
        .enumerate()
        .find(|(_, username)| **username == new_username)
        .map(|(index, _)| index);
    if let Some(index) = res {
        // Use `AnyError::builder()` to create an error with all essential
        // context you'll need.
        let err = AnyError::builder()
            .message("the username already exists")
            .kind(ErrKind::RuleViolation)
            .context("new_username", new_username)
            .context("index", index)
            .build();
        Err(err)
    } else {
        usernames.push(new_username);
        Ok(usernames.len() - 1)
    }
}

fn parse_i32(input: &str) -> AnyResult<i32> {
    // Use `AnyError::wrap()` to wrap any other error type.
    input.parse::<i32>().map_err(AnyError::wrap)
}

以上面的 try_add_username() 为例,假设我们拿到了其返回值,则我们可以这样处理:

use err::*;

fn main() {
    let mut usernames = Vec::new();

    let res = try_add_username(&mut usernames, "foo").unwrap();
    assert_eq!(res, 0);

    let err = try_add_username(&mut usernames, "foo").unwrap_err();
    assert_eq!(err.to_string(), "the username already exists"); // Or `err.message()`.
    assert_eq!(err.kind(), ErrKind::RuleViolation);
    assert_eq!(err.get("new_username"), Some("\"foo\""));
    assert_eq!(err.get("index"), Some("0"));
}

错误包装

以一个模拟访问数据库的代码片段为例子,展示如何使用 OverlayIntermediate helper trait 来包装一个已有的错误。

use err::*;

struct UserRepository {
    conn: Arc<Connection>,
}

impl UserRepository {
    pub fn find_by_username(&self, username: &str) -> AnyResult<User> {
        // Don't build SQL statements yourself in practice.
        let statement = format!("SELECT * FROM users WHERE users.username = '{username}'");
        let data = self.conn.query(&statement)
            .overlay(("could not get a `User` due to SQL execution error", ErrKind::EntityAbsence))
            .context("username", username)
            .context("statement", statement)?;
        let entity = User::try_from(data)
            .overlay(("could not get a `User` due to serialization error", Errkind::EntityAbsence))
            .context("username", username)?;
        Ok(entity)
    }
}

Overlay::overlay() 用于一个 AnyError<C, K>Result<T, AnyError<C, K>> 上,用于指定构造一个新的 AnyError 并将原有的错误包装进去,Overlay::overlay() 的参数可以传入 error message 和 error kind(具体看文档),然后其返回一个中间状态或者说是 builder,可以继续调用 Intermediate::context() 来添加上下文信息,最后借助 ? 传播包装后的错误。

错误报告

考虑如下这样嵌套多次的错误,借助 Report 可以方便地打印错误信息,无论是程序运行中打印到 logger 还是作为程序错误结束的报告,都是可以的:

use err::*;

fn source_error() -> AnyResult<()> {
    let err = AnyError::builder()
        .message("the source error is here")
        .kind(ErrKind::InfrastructureFailure)
        .context("key1", "value1")
        .context("key2", "value2")
        .build();
    Err(err)
}

fn intermediate_error() -> AnyResult<()> {
    source_error()
        .overlay("the intermediate error is here")
        .context("key3", "value3")?;
    Ok(())
}

fn toplevel_error() -> AnyResult<()> {
    intermediate_error()
        .overlay("the toplevel error is here")?;
    Ok(())
}

fn main() -> impl Termination {
    Report::capture(|| {
        toplevel_error()?;
        Ok(())
    })
}

完整示例

以下是一个超简单的 echo server 的例子,其中一些部分为了展示功能,可能会比正常写法略复杂:

mod err {
    use anyerr::context::LiteralKeyStringMapContext;
    use anyerr::AnyError as AnyErrorTemplate;

    pub use anyerr::kind::NoErrorKind as ErrKind;
    pub use anyerr::Report;
    pub use anyerr::{Intermediate, Overlay};

    pub type AnyError = AnyErrorTemplate<LiteralKeyStringMapContext, ErrKind>;
    pub type AnyResult<T> = Result<T, AnyError>;
}

use std::io::{Read, Write};
use std::net::{TcpListener, TcpStream};
use std::process::Termination;
use std::thread;
use std::time::Duration;

use err::*;

const SERVER_IP: &str = "127.0.0.1";
const SERVER_PORT: &str = "8080";

fn main() -> impl Termination {
    Report::capture(|| {
        let listener = TcpListener::bind(format!("{SERVER_IP}:{SERVER_PORT}"))
            .map_err(AnyError::wrap)
            .overlay("could not bind the listener to the endpoint")
            .context("ip", SERVER_IP)
            .context("port", SERVER_PORT)?;

        eprintln!("Started listening on {SERVER_IP}:{SERVER_PORT}");

        for connection in listener.incoming() {
            let Ok(stream) = connection else {
                continue;
            };

            thread::spawn(move || {
                handle_connection(stream).unwrap_or_else(|err| {
                    let report = Report::wrap(err).kind(false);
                    eprintln!("{report}");
                });
            });
        }

        Ok(())
    })
    .kind(false)
}

fn handle_connection(mut stream: TcpStream) -> AnyResult<()> {
    let client_addr = stream
        .peer_addr()
        .map_or("<UNKNOWN>".into(), |addr| addr.to_string());
    let mut buffer = [0u8; 256];
    let mut total_read = 0;

    eprintln!("{client_addr} started the connection");
    thread::sleep(Duration::from_secs(3));

    loop {
        let size_read = stream
            .read(&mut buffer)
            .map_err(AnyError::wrap)
            .overlay("could not read bytes from the client")
            .context("client_addr", &client_addr)
            .context("total_read", total_read)?;
        total_read  = size_read;

        if size_read == 0 {
            eprintln!("{client_addr} closed the connection");
            return Ok(());
        }

        thread::sleep(Duration::from_secs(3));

        let mut cursor = 0;
        while cursor < size_read {
            let size_written = stream
                .write(&buffer[cursor..size_read])
                .map_err(AnyError::wrap)
                .overlay("could not write bytes to the client")
                .context("client_addr", &client_addr)
                .context("total_read", total_read)
                .context("cursor", cursor)?;
            cursor  = size_written;
        }
    }
}

进阶用法

更多用法和 API 请参考 Docs.rs

评论区

写评论
iamazy 2025-02-20 09:19

可以展示Stack backtrace这个功能太棒啦

1 共 1 条评论, 1 页