Rustで独自のエラータイプを実装する必要がある場合、dtolnay/thiserror を使うことで簡単にカスタムエラータイプを実装できます。
この記事では、thiserror
を「利用した実装」と「利用していない実装」を比べることで、どれだけ簡単にカスタムエラータイプを作れるか説明していきます。ちなみに、thiserror
の詳細までは説明しないので、知りたい方はREADMEをご覧ください。
thiserrorの概要
はじめにthiserror
の特徴について簡単に説明します。
thiserror
は標準ライブラリの std::error::Errorトレイトの便利なderiveマクロを提供している- カスタムエラータイプを実装する際の
fmt::Display
、std::error::Error
、From<T>
トレイトの実装をほぼ省略できる - 具体的には、
#error("...")
はfmt::Display
を実装し、#[from]
はFrom
トレイトを実装し、#[source]
やsource
があれば自動的にsource()
メソッドを実装する
// thiserrorクレートを利用したカスタムエラータイプの定義例 #[derive(thiserror::Error)] 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, }
thiserrorを利用した実装
まずは、thiserror
の使い方をイメージできるようにthiserror
を使用してカスタムエラータイプを定義する方法を紹介します。
- データストアのエラーを表す
DataStoreError
というenumを定義。enum内ではthiserror
の機能を使い#[error]
属性を使用してエラーメッセージを定義している。また、#from
属性を使いFrom
トレイトを自動的に実装している。 - カスタムエラーを使うために
connect_data_store()
とget_user()
を定義。connect_data_store()
は、データストアからの切断をシミュレートするためにio::Error
を返す。get_user()
は、connect_data_store()
を呼び出し、Result<(), DataStoreError>
を返す。 main()
では、get_user()
を呼び出してエラーが返ることを確認。assert_eq!
マクロを使用して、エラーメッセージが`#[error]属性で定義した"data store disconnected"であることを確認している。
use std::io; use thiserror::Error; // データストアのエラー #[derive(Error, Debug)] pub enum DataStoreError { // #[error]により`fmt::Error`トレイとの実装が必要ない // #[from]によりFromトレイトの実装が必要ない #[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, } // データベースへの接続 // io::Errorのエラーを返す fn connect_data_store() -> Result<(), io::Error> { Err(io::Error::from(io::ErrorKind::ConnectionAborted)) } // ユーザーの取得 fn get_user() -> Result<(), DataStoreError> { connect_data_store()?; // do something Ok(()) } fn main() { let err = get_user().unwrap_err(); assert_eq!(err.to_string(), "data store disconnected") }
thiserrorを利用していない実装
今度は、上記のコードに対してthiserror
を利用していない実装を説明します。
thiserror
を使わなくなったことで、fmt::Display
トレイトやFrom
トレイトを個別で実装する必要がでてきます。実装内容はほぼ決まりきった内容なのでthiserror
を使うほうが便利ですね。
use std::io; use std::fmt; #[derive(Debug)] pub enum DataStoreError { Disconnect(io::Error), Redaction(String), InvalidHeader { expected: String, found: String }, Unknown, } # fmt::Display を実装する必要がある impl fmt::Display for DataStoreError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Disconnect(_) => write!(f, "data store disconnected"), Self::Redaction(s) => write!(f, "the data for key `{s}` is not available"), Self::InvalidHeader { expected, found } => write!(f, "invalid header (expected {expected:?}, found {found:?})"), Self::Unknown => write!(f, "unknown data store error"), } } } # io::Error の From トレイトを実装する必要がある impl From<io::Error> for DataStoreError { fn from(err: io::Error) -> Self { DataStoreError::Disconnect(err) } } fn connect_data_store() -> Result<(), io::Error> { Err(io::Error::from(io::ErrorKind::ConnectionAborted)) } fn get_user() -> Result<(), DataStoreError> { connect_data_store()?; // do something Ok(()) } fn main() { let err = get_user().unwrap_err(); assert_eq!(err.to_string(), "data store disconnected") }
thiserrorのまとめ
thiserror
を使うことで、カスタムエラーの実装を楽にすることができます。カスタムエラータイプを実装する際のfmt::Display
、std::error::Error
、From<T>
トレイトの実装をほぼ省略できます。ぜひ、使ってみてください。
ここで紹介した以外の便利な機能もあるので、詳細を知りたい方はdtolnay/thiserror のREADMEを見てください。