源错误是否应该在显示输出中包含该源?

Ben*_*son 12 error-handling rust

我有一个隐含该Error特征的错误类型,它包装了一个潜在的错误原因,因此该source方法返回Some(source). 我想知道Display我的错误类型的 impl 是否应该包含该源错误的描述。

我可以看到两个选项:

  1. 是的,包含sourceDisplay输出中,例如“打开数据库时出错:没有这样的文件”

这使得只需通过格式化即可轻松打印整个错误链,"{}"但不可能仅显示错误本身而不显示底层源错误链。它还使该source方法有点毫无意义,并且使客户端代码无法选择如何格式化链中每个错误之间的分隔。然而,在我发现的示例代码中,这种选择似乎很常见。

  1. 不,只需打印错误本身,例如“打开数据库时出错”,并将其留给客户端代码来遍历和显示(source如果客户端代码希望将其包含在输出中)。

这使客户端代码可以选择是否仅显示表面错误或整个链,以及在后一种情况下如何格式化链中每个错误之间的分隔。它给客户端代码留下了遍历链的负担,而且我还没有找到一个规范的实用程序来方便地格式化错误链,其中每个错误都只Display自己排除source。(所以我当然有自己的。)

混乱的箱子(我真的很喜欢)似乎暗示有利于选项 2,因为带有字段source但没有display属性的错误变体默认为格式化Display不包含source.

也许我真正的问题是:该方法的目的是什么source?是为了让格式化错误链更加灵活吗?或者Display真的应该输出用户可见的有关错误的所有内容,并且source仅用于开发人员可见的目的?

我希望看到一些关于此的明确指导,最好是在该Error特征的文档中。

#[derive(Debug)]
enum DatabaseError {
    Opening { source: io::Error },
}

impl Error for DatabaseError {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        match self {
            DataBaseError::Opening { source } => Some(source),
        }
    }
}

impl fmt::Display for DatabaseError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            DatabaseError::Opening { source } => {
                // ??? Should we include the source?
                write!(f, "Error opening database: {}", source)

                // ??? Or should we leave it to the caller to call .source()
                //     if they want to include that in the error description?
                write!(f, "Error opening database")
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

E_n*_*ate 14

tl;dr - 要么在源链中包含底层错误,要么在 上附加源消息Display,但不能同时添加两者。不过,您的里程可能会有所不同。

是否在实现上打印源错误的两个选项Display创建了两种设计流派。这个答案将解释两者,同时客观地陈述它们的主要差异,并澄清一些可能的误解。

设计 1:是的,包含source在您的Displayimpl中

出现问题的示例:

#[derive(Debug, Snafu)]
enum Error {
    #[snafu(display("Could not read data set token: {}", source))]
    ReadToken {
        #[snafu(backtrace)]
        source: ReadDataSetError,
    },
}
Run Code Online (Sandbox Code Playgroud)

正如问题中已经提到的,关键优点是提供全部信息就像打印错误值一样简单。

eprintln!("[ERROR] {}", err);
Run Code Online (Sandbox Code Playgroud)

它简单易行,不需要辅助函数来报告错误,尽管缺乏表示灵活性。如果没有字符串操作,您将永远得到一串以冒号分隔的错误。

[ERROR] Could not read data set token: Could not read item value: Undefined value length of element tagged (5533,5533) at position 3548
Run Code Online (Sandbox Code Playgroud)

设计 2:不,省略source你的Displayimpl

#[derive(Debug, Snafu)]
enum Error {
    #[snafu(display("Could not read data set token"))]
    ReadToken {
        #[snafu(backtrace)]
        source: ReadDataSetError,
    },
}
Run Code Online (Sandbox Code Playgroud)

虽然这不会像以前那样通过单行打印为您提供完整信息,但您可以将该任务留给项目范围的错误报告器。这也为 API 的使用者提供了在错误呈现方面更大的灵活性。

下面是一个简单的例子。需要额外的逻辑来呈现错误的回溯。

fn report<E: 'static>(err: E)
where
    E: std::error::Error,
    E: Send + Sync,
{
    eprintln!("[ERROR] {}", err);
    if let Some(cause) = err.source() {
        eprintln!();
        eprintln!("Caused by:");
        for (i, e) in std::iter::successors(Some(cause), |e| e.source()).enumerate() {
            eprintln!("   {}: {}", i, e);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

操场

与固执己见的图书馆集成的兴趣也值得考虑。也就是说,生态系统中的某些板条箱可能已经对选择哪个选项做出了假设。在 中anyhow,错误报告默认已经遍历错误的源链。当用于anyhow错误报告时,你应该添加source,否则你可能会得到一个令人恼火的重复消息列表:

[ERROR] Could not read data set token: Could not read item value: Undefined value length of element tagged (5533,5533) at position 3548

Caused by:
   0: Could not read item value: Undefined value length of element tagged (5533,5533) at position 3548
   1: Undefined value length of element tagged (5533,5533) at position 3548
Run Code Online (Sandbox Code Playgroud)

同样,该eyre库提供了可定制的错误报告抽象,但eyrecrate 生态系统中现有的错误报告器也假设错误的Display实现不会打印源。

那么,哪一个?

感谢错误处理项目组的努力,Display2021 年初提出了关于实施的关键指南:

具有源错误的错误类型应该通过返回该错误source或在其自己的输出中包含该源的错误消息Display,但决不能两者兼而有之。

这将是第二种设计:避免在实现中附加源的错误消息Display。SNAFU 用户可以使用其报告 API(自版本 0.7.2起可用),或引入自定义错误报告器。由于围绕这一准则的生态系统尚未成熟,人们可能仍然会发现错误实用程序箱缺乏对这种方式的错误报告的支持。

在任一情况下...

该决定仅在错误报告中起作用,而不在错误匹配或以其他方式处理错误中起作用。该方法的存在在所有错误类型上source建立了一个链式结构,可用于模式匹配和后续的程序流程控制。Error::source无论如何报告错误,该方法在生态系统中都有其用途。

此外,最终由开发人员选择如何设计他们的错误和各自的Display实现,尽管一旦开始与其他组件集成,遵循指南将是实现一致错误报告的正确方法。

Rust API 指南怎么样?

Rust API 指南没有提出关于Display错误的意见,除了C-GOOD-ERR之外,它只指出错误类型的Display消息应该是“小写,没有尾随标点符号,并且通常简洁”。有一个待定的提案source来更新此指南,指示开发人员在其实现中排除Display。然而,拉取请求是在指南提出之前创建的,并且从那时起(在撰写本文时)就没有更新过。

也可以看看: