100
58

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

rust のエラーライブラリは failure を使わないでください

Last updated at Posted at 2018-06-29

※ 2020-04-27 追記 failure はもはやメンテされていません
代わりに後継の thiserroranyhow などを使ってください

続編 Rustのエラーまわりの変遷






























































































































動機

Rust で何かライブラリを作ったらエラーを定義設計する必要があります。
パースエラー、ネットワークエラー、etc...
例えば、

pub fn get_value_from_db(&self, key: &str) -> Result<String, Error>

のような API を公開するときに、 Result<String, Error>Error をどうしようかという問題です。

2018 年 6 月現在の Rust のエラー設計のベストプラクティスは failure - https://github.com/rust-lang-nursery/failure - です。

補足情報

使い方

※ この記事はライブラリ作者向けです。普段使いで unwrap しててエラー設計が必要ないという人は failure::Error 構造体を使ってください - https://boats.gitlab.io/failure/use-error.html

ErrorKind を定義する

まず enum ErrorKind を定義します。
このとき 依存ライブラリのエラーも一緒に定義します。

error.rs
#[derive(Fail, Debug)]
pub enum ErrorKind {
    #[fail(display = "IO error")]
    Io,
    #[fail(display = "Serde error")]
    Serde,
    #[fail(display = "Hyper error")]
    Hyper,
    #[fail(display = "Cannot parse uri")]
    UrlParse,
    #[fail(display = "askama error")]
    Askama,
    #[fail(display = "service error")]
    Service,
}

この Fail トレイトがこのライブラリが提供する新しいエラー表現です。

derive できないコードのボイラープレートを書く

次に以下のコードをコピペします。

error.rs
/* ----------- failure boilerplate ----------- */

use std::fmt;
use std::fmt::Display;
use failure::{Backtrace, Context, Fail};

#[derive(Debug)]
pub struct Error {
    inner: Context<ErrorKind>,
}

impl Fail for Error {
    fn cause(&self) -> Option<&Fail> {
        self.inner.cause()
    }

    fn backtrace(&self) -> Option<&Backtrace> {
        self.inner.backtrace()
    }
}

impl Display for Error {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        Display::fmt(&self.inner, f)
    }
}

impl Error {
    pub fn new(inner: Context<ErrorKind>) -> Error {
        Error { inner }
    }

    pub fn kind(&self) -> &ErrorKind {
        self.inner.get_context()
    }
}

impl From<ErrorKind> for Error {
    fn from(kind: ErrorKind) -> Error {
        Error {
            inner: Context::new(kind),
        }
    }
}

impl From<Context<ErrorKind>> for Error {
    fn from(inner: Context<ErrorKind>) -> Error {
        Error { inner }
    }
}

ここ https://boats.gitlab.io/failure/error-errorkind.html にかかれている通り、現状の failure 0.1 crate をうまく使うにはボイラープレートが伴います。
このボイラープレートは脳死でコピペしてください。マイクロソフトの iotedge の edgelet もコピペしています。

現在このボイラープレートをマクロにしようという提案がされており、 1.0 では入るかもしれません。 - https://github.com/rust-lang-nursery/failure/issues/140#issuecomment-362439068

エラー間の変換を書く

依存ライブラリのエラーを定義しようとしている自前のエラーへ変換するコードを書きます。
これを書いておくと依存ライブラリのエラーを自前のエラーへ .map_err(Into::into) で変換できるようになります。

error.rs
use failure::SyncFailure;
use hyper::Error as HyperError;
use askama::Error as AskamaError;
use std::io::Error as IOError;

impl From<IOError> for Error {
    fn from(error: IOError) -> Error {
        Error {
            inner: error.context(ErrorKind::Io),
        }
    }
}

impl From<HyperError> for Error {
    fn from(error: HyperError) -> Error {
        Error {
            inner: error.context(ErrorKind::Hyper),
        }
    }
}

impl From<UrlParseError> for Error {
    fn from(error: UrlParseError) -> Error {
        Error {
            inner: error.context(ErrorKind::UrlParse),
        }
    }
}

impl From<SyncFailure<AskamaError>> for Error {
    fn from(error: SyncFailure<AskamaError>) -> Error {
        Error {
            inner: error.context(ErrorKind::Askama),
        }
    }
}

現在の askama 0.5.0 のエラーには error-chain を利用していますが、error-chain が使用する std の Error トレイトは Fail トレイトが要求する Sync を満たしません。 - https://docs.rs/askama/0.7.0/askama/enum.Error.html#why-not-failureerror-chain
そのような場合は SyncFailure で包んでやる必要があります - https://github.com/rust-lang-nursery/failure/issues/109#issuecomment-350920299

※ askama 0.7.0 では error-chain の使用を辞めるようです - https://github.com/djc/askama/issues/92

ライブラリを使用するコードを書く

ライブラリのエラーを↑で定義した自前の struct Error に変換します。

通常は .map_err(Into::into) する。 以下の例は impl From<UrlParseError> for Error を実装しているので .map_err(Into::into) できます。

serde_urlencoded::from_bytes(&buf).map_err(Into::into)

Sync を満たさないエラーは .map_err(SyncFailure::new).map_err(Into::into) する。
次の例は impl From<SyncFailure<AskamaError>> for Error を実装しているので into できます。

IndexTemplate { entries }.render().map_err(SyncFailure::new).map_err(Into::into)

hyper で書いた Web サーバでの使用例です。

main.rs
fn handler(ctx: service::Posts, req: Request<Body>) -> Box<Future<Item=Response<Body>, Error=Error> + Send + 'static> {
    let mut res = Response::new(Body::empty());
    match (req.method(), req.uri().path()) {
        (&Method::GET, "/") => {
            #[derive(Deserialize)]
            struct Query {
                offset: u64,
                limit: u64,
            }
            let fut = mdo!{
                let query = req.uri().query().unwrap_or("offset=0&limit=100");
                Query{ offset, limit } =<< future::result(serde_urlencoded::from_str(query)).map_err(Into::into);
                (_len, lst) =<< ctx.list(offset, limit).map_err(Into::into);
                let entries = lst.iter().map(|o| Entry{
                    timestamp: DateTime::from_utc(o.timestamp, Utc),
                    username: o.author.to_string(),
                    message: o.body.to_string()
                }).collect();
                tmp =<< future::result(IndexTemplate { entries }.render()).map_err(SyncFailure::new).map_err(Into::into);
                let _ = *res.body_mut() = Body::from(tmp);
                ret future::ok(res)
            };
            Box::new(fut)
        },
        (&Method::POST, "/") => {
            #[derive(Deserialize)]
            struct FormData {
                username: String,
                message: String,
            }
            let fut = mdo!{
                let body = req.into_body();
                buf =<< body.concat2().map_err(Into::into);
                FormData{ username, message } =<< future::result(serde_urlencoded::from_bytes(&buf)).map_err(Into::into);
                _ =<< ctx.create(&username, &message).map_err(Into::into);
                let _ = res.headers_mut().insert(LOCATION, HeaderValue::from_static("/"));
                let _ = *res.status_mut() = StatusCode::SEE_OTHER;
                ret future::ok(res)
            };
            Box::new(fut)
        },
        _ => {
            *res.status_mut() = StatusCode::NOT_FOUND;
            Box::new(future::ok(res))
        }
    }
}

補足1 mdo-future について

future::result(serde_urlencoded::from_bytes(&buf)).map_err(Into::into) としているのは mdo-future - https://github.com/danslapman/rust-mdo-future - を使って非同期エラーと混ぜて使いたいからです。実際便利。

補足2 FailFuture

最新の tokio runtime は work stealing アルゴリズムでタスクキューに溜まった future をスレッドプールで処理します。
そのため future がどのスレッドで実行されるのかは実行時に決まります。
なので Box<Future<Item=Response<Body>, Error=Error> + Send + 'static> のように Send をつけてやる必要があります。
Fail トレイトは Display + Debug + Send + Sync + 'static を要求するのでスレッドセーフです - https://docs.rs/failure/0.1.1/failure/trait.Fail.html

エラーハンドリングを書く

Web サーバを書いているとエラーに応じてレスポンスのステータスコードを変えたくなります。
以下の例は hyper で書いた Web サーバでエラーハンドリングをする例です。

handler(srv.clone(), req).then(error_handler) のように .then でつなげて書くことを意図しています。

main.rs
fn error_handler(ret: Result<Response<Body>, Error>) -> Box<Future<Item=Response<Body>, Error=hyper::Error> + Send + 'static> {
    match ret {
        Ok(res) => Box::new(future::ok(res)),
        Err(err) =>{
            let mut fail: &Fail = &err;
            let mut message = err.to_string();
            
            while let Some(cause) = fail.cause() {
                message.push_str(&format!("\n\tcaused by: {}", cause.to_string()));
                fail = cause;
            }
            let status_code = match *err.kind() {
                ErrorKind::UrlParse | ErrorKind::Hyper => StatusCode::BAD_REQUEST,
                _ => StatusCode::INTERNAL_SERVER_ERROR,
            };

            let body = json!({
                "message": message,
            }).to_string();

            let res: Response<Body> = Response::builder()
                .status(status_code)
                .header(CONTENT_TYPE, "application/json")
                .header(CONTENT_LENGTH, body.len().to_string().as_str())
                .body(body.into())
                .expect("response builder failure");

            Box::new(future::ok(res.map(Into::into)))
        }
    }
}

このようにエラーの原因を遡って表示できます。

{"message":"service error\n\tcaused by: db error\n\tcaused by: diesel query result error\n\tcaused by: attempt to write a readonly database"}

このWebサーバはビジネスロジックを記述した service クレートを使用していますが、その中で DB アクセスを抽象化した DB クレートでエラーが起きており、 さらにその中の diesel を使っている部分がエラーを返していることがこのエラーからわかります。これらはすべて failure を使っているので統一的に扱えています。

このエラーの原因を遡る方法はこの部分で実現されています。

let mut fail: &Fail = &err;
let mut message = err.to_string();

while let Some(cause) = fail.cause() {
    message.push_str(&format!("\n\tcaused by: {}", cause.to_string()));
    fail = cause;
}

fail.cause() はそのエラーの根本原因を返します。
それを fail = cause; で代入してループすることでより深い原因を探っています。

fail.causes() はイテレータを返すので、人によってはそれを fold したほうがわかりやすいかもしれません - https://docs.rs/failure/0.1.1/failure/trait.Fail.html#method.causes

バックトレースがほしい場合は この cause に対して backtrace() してやると、そのエラーが backtrace を持ってい場合は Option<&Backtrace>Some が返ります。 - https://docs.rs/failure/0.1.1/failure/trait.Fail.html#method.backtrace

ドキュメントのわかりくさについて

公式のドキュメント - https://boats.gitlab.io/failure/ - がわかりにくいという issue - https://github.com/rust-lang-nursery/failure/issues/140#issuecomment-369963154 - が立っており、現在ここ - https://github.com/rust-lang-nursery/failure/issues/209#issuecomment-394914709 - で新しいドキュメントの草稿が練られています。

感想

バージョン情報

  • serde_urlencoded 0.5.2
  • failure 0.1.1
  • failure_derive 0.1.1
  • askama 0.5.0
  • futures 0.1.21
  • hyper 0.12.3
  • rustc 1.27.0 (3eda71b00 2018-06-19)

最新情報への誘導リンク

100
58
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
100
58

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?