⚠️

thiserror crate の基本的な使い方

に公開

前回は Errordowncast について書きました 。今回は thiserror crate の基本的な使い方について書きます。

thiserror crate とは

thiserrorstd::error::Error trait の実装のための derive macro を提供する crate です。

thiserror は Rust におけるエラー関連の定番 crate のひとつだと思います。

std::error::Error trait については過去に書きました

derive macro についてはそのうち書きます(たぶん)。

インストール方法

cargo add thiserror くらい。特に変わったところはないです。

使い方: unit struct の例

#[derive(Debug, thiserror::Error)]
#[error("unit struct error")]
struct UnitStructError;

fn assert_impls<T: std::fmt::Debug + std::fmt::Display + std::error::Error>() {}
assert_impls::<UnitStructError>();
assert_eq!(UnitStructError.to_string(), "unit struct error");

thiserror::Error#[derive(...)] に指定します。

#[error("...")] という属性の指定は Display の format になります。

これで std::error::Errorstd::fmt::Display が実装されます。

std::error::ErrorDebug + Display を要求するので #[derive(...)] には Debug も指定します。

使い方: tuple struct + #[from] + {0} の例

#[derive(Debug, thiserror::Error)]
#[error("tuple struct error: {0}")]
struct TupleStructError(#[from] std::io::Error);

assert_eq!(
    format!("{}", TupleStructError(std::io::Error::other("error1"))),
    "tuple struct error: error1"
);
assert_eq!(
    format!(
        "{}",
        // `#[from]` は `impl From<std::io::Error> for TupleStructError` を提供する
        TupleStructError::from(std::io::Error::other("error2"))
    ),
    "tuple struct error: error2"
);
assert_eq!(
    format!(
        "{:?}",
        // `#[from]` のフィールドは `std::error::Error` trait の `source()` にも使用される
        TupleStructError(std::io::Error::other("error3")).source()
    ),
    "Some(Custom { kind: Other, error: \"error3\" })"
);

今度は tuple struct 。エラーの理由を残したいことも多いのでこの形もままあると思います。

今回は #[from] 属性も指定しています。

これは From trait の実装を提供してくれます。これで ? operator を使いやすくなりますね。

? operator も From trait も過去に書いてないですね、そのうち書きます(たぶん)。

#[from]Error trait の source() にも自動で使われます。

あとは #[error("...")] に tuple の field を指定しています。 {0} の箇所がそうです。

format! macro と似たような形なので違和感なく使えると思います。

使い方: named fields struct + #[source] + {var:?} の例

#[derive(Debug, thiserror::Error)]
#[error("struct error: {cause:?}")]
struct StructError {
    #[source]
    cause: std::io::Error,
    // `#[source]` の代わりに↓のようなフィールド名 `source` でも良い
    // source: std::io::Error,
}

assert_eq!(
    format!(
        "{}",
        StructError {
            cause: std::io::Error::other("error1")
        }
    ),
    "struct error: Custom { kind: Other, error: \"error1\" }"
);
// `#[source]` は `From` trait を実装しないので、↓はコンパイルエラーになる
// StructError::from(std::io::Error::other("error2"))
assert_eq!(
    format!(
        "{:?}",
        // `#[source]` のフィールドは `std::error::Error` trait の `source()` にも使用される
        StructError {
            cause: std::io::Error::other("error2")
        }
        .source()
    ),
    "Some(Custom { kind: Other, error: \"error2\" })"
);

今度は named field struct 。

フィールド名があるときの変数は {var} の形で書きます。

さらに、ここでは {var:?} を使用しています。これは Debug 出力を使用します。

tuple struct のときの {0} も同様に {0:?} のように書いて Debug 出力を使用できます。

#[source] 属性を指定すると、そのフィールドが source() に使用されます。

#[from] と異なり、 From trait の実装は提供されません。これは #[from] の型が衝突してしまうと From trait を実装できなくなるので、それを回避するのに使ったりします。

使い方: enum + #[error(transparent)] の例

#[derive(Debug, thiserror::Error)]
enum EnumError {
    #[error("io error")]
    Io(#[source] std::io::Error),
    #[error(transparent)]
    From(#[from] Box<dyn std::error::Error + Send + Sync>),
    #[error(transparent)]
    Unknown(Box<dyn std::error::Error + Send + Sync>),
}

assert_eq!(
    format!("{}", EnumError::Io(std::io::Error::other("error1"))),
    "io error"
);
assert_eq!(
    format!(
        "{}",
        EnumError::from(Box::<dyn std::error::Error + Send + Sync>::from("error2"))
    ),
    "error2"
);
assert_eq!(
    format!(
        "{:?}",
        EnumError::from(Box::<dyn std::error::Error + Send + Sync>::from("error2")).source()
    ),
    "None"
);
assert_eq!(format!("{}", EnumError::Unknown("error3".into())), "error3");

enum の例です。 variant ごとに #[error("...")] などを指定できます。

#[error(transparent)] は内部のフィールドのものを透過的に使用します。

この例だと Box<dyn std::error::Error + Send + Sync> として "error3" を持っているので、それを to_string() すると、そのまま "error3" と表示されます。

transparent なので、 EnumError の中に Box<...> を持っているように見せるのではなく Box<...> かのように見せるので #[source] は使用できません。 transparent variant can't contain #[source] というエラーになります。

一方で transparent のとき、 #[from] は使用できます。ただし、 source() としては提供されず、 From trait の実装のみ提供されます。

通常は #[from]#[source] を兼ねている (しかも同時に指定できない) ので、すこしややこしいですね。

次回予告

#[error("...", var = ...)] のような追加の format! の引数や #[backtrace] については書いていませんが、thiserror の基本的な使い方は書けたと思います。

これらの内容は https://github.com/dtolnay/thiserror のリポジトリの README にもっと簡潔かつ漏れなくまとめられているので、そちらを読むのをおすすめします。

次回は cargo-expand で実際に展開されたマクロを見てみたいと思います。

参考

GitHubで編集を提案
ドクターメイト

Discussion