Rust のエラーライブラリといえばfailureです。 failure の公式ドキュメントにはError と ErrorKind ペアというページがあります。
failure::Error
にしておくと便利です。これはスタックトレースの表示などに使います。エラーをハンドリングするということはよくあるが、エラー型が異なる場合に(例えば DB エラーと serialization エラーなど)型が違うものをハンドリングするのは困難なので、ErrorKind というラベルをつけておくというテクニックです。
例えば API サーバーでエラーに識別子をつけておき、クライアントサイドで解釈してよしなにエラー処理を行うなどしようとするとこのようなやり方を採りたくなります。
問題点:
しかしドキュメントにある方法では、ErrorKind は 1 つの巨大な enum であることが想定されています。これはエラーを 1 箇所に集めておく(あるいは、少なくとも 1 箇所から辿れるようにしておく必要がある)必要があるという意味で不便な方法です。エラーはいろいろなファイルでいろいろな種類のものが起きうるので、ファイルごとに異なる ErrorKind を定義できるようにしたくなりました。
上のような問題点を解決すべく、次のようなやり方を採用しています。
TypeId
を載せておく// ErrorKind(error_type) and Error(inner) pair
pub struct ServiceError {
type_id: std::any::TypeId,
error_type: String,
inner: Error,
}
pub trait IServiceError: Any {
fn error_type(&self) -> String {
"internal_server_error".to_string()
}
}
impl ServiceError {
pub fn new<E>(err: impl IServiceError, detail: E) -> ServiceError
where
failure::Error: From<E>,
{
ServiceError {
type_id: err.type_id(),
error_type: err.error_type(),
status_code: err.status_code(),
inner: From::from(detail),
}
}
pub fn only(err: impl IServiceError) -> ServiceError {
ServiceError {
type_id: err.type_id(),
error_type: err.error_type(),
status_code: err.status_code(),
inner: failure::err_msg("error"),
}
}
pub fn is_error_of(&self, err: impl IServiceError) -> bool {
self.type_id == err.type_id() && self.error_type() == err.error_type()
}
}
TypeId
という型は聞き慣れないかもしれませんが、標準ライブラリのAny traitで定義されているメソッドから取得できます。
Rust コンパイラは型に対してTypeId
という一意な識別子を割り振っており、これによって 2 つの型が等しいかどうかを判定することが出来ます。
ここでは ErrorKind の代わりとして、error_type
文字列を自力で定義することで替えることにしています。しかし、さすがにただの文字列だと意図せず被ってしまうケースがあり事故を起こしそうなので、定義元の型のtype_id
を保存しておくことで意図しない型とエラーが被ってしまうことを防いでいます。
エラーを定義する側のサンプルコードです。
pub enum HogeError {
InvalidRequest,
AccessDenied,
IOError,
}
impl IServiceError for HogeError {
fn error_type(&self) -> String {
use HogeError::*;
match self {
InvalidRequest => "invalid_request",
AccessDenied => "access_denied",
IOError => "io_error",
}
.to_string()
}
}
エラーは基本的に error_type の文字列を定義するだけです。エラー型はHogeError
のように 0 引数コンストラクタを並べることを想定しているので、ここの実装はマクロ等で自動生成しても良いかもしれません。
エラーの生成とハンドリングは次のように行うことが出来ます。
fn must_fail() -> Result<(), ServiceError> {
Err(ServiceError::only(HogeError::IOError))
}
fn recover_io_error() -> Result<(), ServiceError> {
let result = do_something();
match result {
Err(err) if err.is_error_of(HogeError::IOError) => {
// IOErrorの場合はrecoverする
},
r => r,
}
}
上で定義したServiceError::is_error_of
メソッドが、
result
の中の例外がHogeError
型であることio_error
であること(今の場合はHogeErrror::IOError
から来たもの)の 2 つをチェックしています。
このHogeError
は実際には異なるモジュールで自由に定義することが出来、複数のエラーを混ぜて書けるようになり便利です。
Any
trait にはtype_id
だけでなくdowncast_ref
などのキャストを行うメソッドが用意されています。これを用いてServiceError
自体のダウンキャストを行うことを最初は考えていたのですが、downcast_ref
は対象のオブジェクトのライフタイムが static であることを要求します。これにより参照や参照を含むデータのダウンキャストが行えないため、今回は断念して error_type として文字列を定義するという原始的な方法に落ち着きました。
(ServiceError
自体のダウンキャストができると error_type 文字列を定義する必要もなくなるので更に便利になりそうです)