Rust のエラーまわりの変遷

Rust LT #6 で発表したスライド


Error トレイトについて



std::error::Error トレイトとは



Error トレイト

こんなの

pub trait Error: Debug + Display {

fn description(&self) -> &str;
fn cause(&self) -> Option<&dyn std::error::Error>;
}



Error::description でエラーの内容を表示できる

println!("{}", err.description());



Error::cause でエラーの元を辿れる (cause chain)

let mut cause = err.cause();

while let Some(err) = cause {
println!("{}", err.description());
cause = err.cause();
}



Error トレイトの問題点



1. ErrorDebugDisplay トレイトを実装しないといけない

Error を derive できない(※当時は derive macro などなかった)


#[derive(Debug)]

enum MyError {
Io(std::io::Error),
Parse(std::num::ParseIntError),
}

// ボイラープレート
// pub trait Error: Debug + Display { ... }
impl std::error::Error for MyError {
fn description(&self) -> &str {
match *self {
MyError::Io(ref err) => err.description(),
MyError::Parse(ref err) => err.description(),
}
}
fn cause(&self) -> Option<&std::error::Error> {
match *self {
MyError::Io(ref err) => Some(err),
MyError::Parse(ref err) => Some(err),
}
}
}

impl std::fmt::Display for MyError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match *self {
MyError::Io(ref err) => write!(f, "ファイル開けへんやんけ: {}", err),
MyError::Parse(ref err) => write!(f, "パースできへんやんけ: {}", err),
}
}
}



2. スタックトレースがとれない

このエラーはどこから来たのかなんもわからん



3. Error::descriptionDisplay で役割が被ってる

// 何が違うの?

println!("{}", err.description());
println!("{}", err);



4. cause チェーンがイテラブルでない

// なぜそこで while

let mut cause = err.cause();
while let Some(err) = cause {
cause = err.cause();
}



5. 元のエラー型にダウンキャストできない

// Error::cause の戻り値に 'static がついてない

let cause: Option<&Error> = err.cause();



6. Send も Sync も 'static ない

tokio などで使うのが大変



結論: Error トレイトは問題だらけなので……


  • error-chain


    • 問題だらけの Error を使うためのベストプラクティス



  • failure


    • そもそも Error なんて使わんきゃええ



  • RFC2540, "Fix the Error Trait"


    • いっそ Error を改善しよう





error-chain について



error-chain クレート



error_chain! マクロ

マクロで Error トレイトのボイラープレートを一括生成できる


error_chain!{

types { MyError, MyErrorKind, MyResultExt, MyResult; }
links {
Another(AnotherError, AnotherErrorKind);
}
foreign_links {
Io(::std::io::Error);
}
errors {
CannotOpenFile(path: String) {
description("cannot open file")
display("CannotOpenFile: '{}'", path)
}
}
}



この error_chain! マクロで生成されるコードは……



Error トレイトを実装した MyError 構造体

pub struct MyError { ... }

impl MyError { ... }
// Error トレイト の実装
impl Error for MyError { ... }
impl Debug for MyError { ... }
impl Display for MyError { ... }



エラー原因を保持するための MyErrorKind 列挙体

pub enum MyErrorKind {

Msg(String),
Another(AnotherErrorKind),
Io(::std::io::Error),
CannotOpenFile(path: String),
}
impl MyErrorKind { ... }
impl Debug for MyErrorKind { ... }
impl Display for MyErrorKind { ... }



error_chain::ChainedError 拡張トレイトの実装

impl error_chain::ChainedError for MyError { ... }



ChainedErrorResult で使えるようにする ResultExt トレイトの実装

pub trait MyResultExt<T> { ... }

pub type MyResult<T> = Result<T, MyError>;



From トレイトの実装

impl From<MyErrorKind> for MyError { ... }

impl<'a> From<&'a str> for MyError { ... }
impl From<String> for MyError { ... }
impl From<another_errors::Error> for MyError { ... }
impl<'a> From<&'a str> for MyErrorKind { ... }
impl From<String> for MyErrorKind { ... }
impl From<MyError> for MyErrorKind { ... }
impl From<another_errors::ErrorKind> for MyErrorKind { ... }



この膨大なコードのおかげで……



1. enum ErorrKind に原因のエラーを持てる

pub enum MyErrorKind {

Msg(String),
Another(AnotherErrorKind),
Io(::std::io::Error),
CannotOpenFile(path: String),
}



2. エラーチェーンを積める

use std::fs::File;

use errors::{MyError, MyErrorKind, MyResultExt};
try!(File::open("foo.txt")
.map_err(MyErrorKind::Io)
.chain_err(|| "ファイル開けへんやんけ"));
.chain_err(|| MyErrorKind::CannotOpenFile("foo.txt".to_string)));
// CannotOpenFile -> Msg -> Io -> std::io::Error



3. MyError::iter で原因をイテレートできる

for err in err.iter() {

println!("{}", err);
}



4. MyError::backtrace でバックトレースがとれる

// need RUST_BACKTRACE=1

if let Some(trace) = err.backtrace() {
// io::Error ではなく MyError が作られた時点のトレースが得られる
println!("{:?}", trace);
}



error-chain の問題点


  • derive macro なんてなかった

  • 生成されるコードが 膨大

  • 初見殺し

  • 生成される MyError!Sync で使い勝手が悪い



結論: error-chain は過去の遺産



じゃあ failure を使えばいいのか?



failure について



failure クレート


  • 2017 年 11 月に登場

  • 問題だらけの Error トレイトを置き換えるために開発された


  • Error トレイトの代わりに Fail トレイトを導入


  • Box<dyn Error> の代わりに failure::Error 構造体を導入



derive できる

#[derive(Debug, Fail)]

pub enum MyError {
#[fail(display = "Input was invalid UTF-8 at index {}", _0)]
Utf8Error(usize),
#[fail(display = "IoError: {}", _0)]
Io(#[cause] io::Error),
}



cause chain がイテラブル

impl Fail {

pub fn iter_causes(&self) -> Causes { ... }
...
}



backtrace が取れる

pub trait Fail: Display + Debug + Send + Sync + 'static {

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



FailSend + Sync + 'static がついてる

tokio でも使える

pub trait Fail: Display + Debug + Send + Sync + 'static {

...
}



ダウンキャストできる

Fail'static がついてるので

pub trait Fail: Display + Debug + Send + Sync + 'static {

...
}
impl Fail {
pub fn downcast_ref<T: Fail>(&self) -> Option<&T> { ... }
pub fn downcast_mut<T: Fail>(&mut self) -> Option<&mut T> { ... }
...
}



cause chain を積める

err

.context(format_err!("Error code: なんかエラーおきた}"))
.context("なんかエラーおきた".to_string());



結論: failure はすごくいいが……



RFC 2504, "Fix the Error trait"



新しい Error トレイト



新しい Error トレイト


  • backtrace できる

  • ダウンキャストできる

trait Error: Display + Debug {

fn backtrace(&self) -> Option<&Backtrace>;
fn source(&self) -> Option<&dyn Error + 'static>;
}



新しい Error トレイト

以前のメソッドがひとつも残ってない

trait Error: Display + Debug {

fn backtrace(&self) -> Option<&Backtrace>;
fn source(&self) -> Option<&dyn Error + 'static>;
}



未解決の問題


  • Backtrace の具体的な API が未定

  • イテラブルな cause chain の API が未定


  • derive(Error) できない



2019年6月現在



error-chain は開発停止



failure は凍結


  • メジャーアップデートはは実装が落ち着くまで停止中 (まるで futures-0.1 のよう?)



    • Fail -はErrorExt にして新 Error トレイトを継承するかも


    • failure::Errorfailure::DefaultError になるかも





ポスト failure が増殖中

などなど



おわり



Appendix. Rust のエラーまわりの歴史


2014


2015


2016


2017


2018


2019