Rustで独自のエラータイプの実装を楽にするthiserrorの使い方
Rustで独自のエラータイプを実装する必要がある場合、thiserror を使うことで簡単にカスタムエラータイプを実装できます。 この記事では、thiserrorを「利用した実装」と「利用していない実装」を比べることで、どれだけ簡単にカスタムエラータイプを作れるか説明していきます。
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を見てください。