thiserror crate の基本的な使い方
前回は Error の downcast について書きました 。今回は thiserror crate の基本的な使い方について書きます。
thiserror crate とは
thiserror は std::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::Error と std::fmt::Display が実装されます。
std::error::Error は Debug + 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 で実際に展開されたマクロを見てみたいと思います。
追記: 次回『 cargo-expand を使ってみよう』を書きました。
Discussion