Rust 錯誤處理函式庫推薦:Anyhow + thiserror

Wu Yu Wei published on
7 min, 1238 words

在開始講解前我想開門見山講結論,Rust 在針對錯誤處理已經有 Result<T> 這個非常好用的型態,而且在 TRPL 中也有一章在專門講解了。如果只是要寫個簡單的測試你可以只定義一個基本的 enum,而如果是正式的專案也更推薦定義好屬於自己的錯誤型態,依賴第三方的函式庫總是會發生不可預期的狀況出現。但有時候在錯誤處理方面的確會遇到一些障礙是我們得花時間特別去解決的,如果這是一個普遍的問題通常大家便會用些方便的依賴來解決。在過去一兩年我想最常用的就是 failure 了,而過去幾年這類型的 crate 更是如雨後春筍般冒出來,再過不久大概就能像 go 和 npm 有上百萬種錯誤處理的函示庫了(笑)。總之目前更好更方便的選擇就是這次要講的 anyhow + thierror

anyhow

anyhow 首次公開於 2019 年十月,它主要是一個能夠讓我們方便傳遞錯誤的函示庫,就像 failure 的訴求之一一樣。不同於 failure 用了自己定義的 trait Failanyhow 定義的形態和 trait 都是基於標準函式庫的 std::error::Error,除此之外無其他依賴,所以他也同時號稱是更好的 Box<dyn Error>。不過也因如此 anyhow 用到了 RFC 2504 的一些改進,所以會需要 Rust 1.34+ 以上>。

首先最重要的當然就是更方便的錯誤型態 Result<T, anyhow::Error> 或者也等同於 anyhow::Result<T>,只要在函式內有實作 std::error::Error trait 的型態,都能夠只使用 ? 來回傳:

use anyhow::Result;

fn get_cluster_info() -> Result<ClusterMap> {
    let config = std::fs::read_to_string("cluster.json")?;
    let map: ClusterMap = serde_json::from_str(&config)?;
    Ok(map)
}

再來 anyhow 也提供了擴展 Result 增加更多 Context(上下文) 的功能。要是遇到錯誤時我們只能看到 "No such file or directory" 的話還是很難知道錯在哪。Context 能提供更高階的錯誤形式來幫助 debug。

use anyhow::{Context, Result};

fn main() -> Result<()> {
    ...
    it.detach().context("Failed to detach the important thing")?;

    let content = std::fs::read(path)
        .with_context(|| format!("Failed to read instrs from {}", path))?;
    ...
}
Error: Failed to read instrs from ./path/to/instrs.json

Caused by:
    No such file or directory (os error 2)

anyhow 也有 Downcasting 能夠用 call by value, shared reference, mutable reference 等方式取得。除此之外還有 Chain 能夠方便迭代出結果

pub fn underlying_io_error_kind(error: &Error) -> Option<io::ErrorKind> {
    for cause in error.chain() {
        if let Some(io_error) = cause.downcast_ref::<io::Error>() {
            return Some(io_error.kind());
        }
    }
    None
}

最後我們常見的各式錯誤處理 macro 也都有提供,像是 bail!, ensure! 以及將字串轉換成錯誤的 anyhow!(相當於 format_err!):

return Err(anyhow!("Missing attribute: {}", missing));

thiserror

不過這樣只涵蓋到了動態錯誤處理的部份,如果你想要用外部依賴來定義出架構完善的錯誤的話,那麼就是使用 thiserror 的時候了。它和 anyhow 都是由 dtolnay 在同時間釋出的,某方面來說可以把它們想成是原本 faiure 拆開來後並改善後的版本。thiserror 最主要的功能就是透過 #[derive(Error)] 來幫你的型態實作 std::error::Error:

use thiserror::Error;

#[derive(Error, Debug)]
pub enum DataStoreError {
    #[error("data store disconnected")]
    Disconnect(#[from] io::Error),
    #[error("the data for key `{0}` is not available")]
    Redaction(String),
    #[error("invalid header (expected {expected:?}, found {found:?})")]
    InvalidHeader {
        expected: String,
        found: String,
    },
    #[error("unknown data store error")]
    Unknown,
}

std::error::Error

最後的最後筆者覺得還是講一下標準函示庫裡最基本的 Error 比較好,在一開始學習實在完全不清楚的情況下,的確都過第三方依賴能夠輕鬆解決錯誤處理,而這也是大多數社群會推薦的。但隨著經驗累積後不免會發現一些負擔,如果時間充裕當然推薦親自實作在標準函式庫內的錯誤型態。而 Error trait 隨著這幾年也有一些轉變,以下是值得注意的改變:

  • Error::description 已經被半棄用中,因為直接 impl Display 會更好
  • Error::cause 已被棄用,因為有了 Error::source
  • Error::iter_chainError::iter_sources 可在 nightly 上使用了
  • Error::backtrace 以及 module 模組也能在 nightly 上使用了

除了 Error 外再加上原本的 Result? 到詳盡的錯誤代碼。這是筆者覺得 Rust 在錯誤處理做得十分不錯的地方,雖然對於一個通用的錯誤處理函示庫該有什麼樣的功能,社群仍在摸索當中。但底層的基本架構的確是非常的穩固完善。