💬

thiserrorでカスタムエラーを実装する

2022/04/16に公開

Rustでライブラリなどを作成していくと、エラー処理のために独自のエラー型を作ることがあります。
例えば下記コードのように、Vecが空のときに返されるOption型のNoneを独自のエラー型に変換したり、ParseIntErrorをラップして、独自の型としてエラーメッセージなどに追加の情報を足したりできます。

main.rs
use std::error::Error as StdError;
use std::fmt;
use std::num::ParseIntError;

type Result<T> = std::result::Result<T, CustomError>;

#[derive(Debug)]
enum CustomError {
    EmptyVec,
    Parse(ParseIntError),
}

impl fmt::Display for CustomError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match *self {
            CustomError::EmptyVec => write!(f, "list not empty"),
            CustomError::Parse(_) => write!(f, "invalid list element"),
        }
    }
}

impl StdError for CustomError {
    fn source(&self) -> Option<&(dyn StdError + 'static)> {
        match *self {
            CustomError::EmptyVec => None,
            CustomError::Parse(ref e) => Some(e),
        }
    }
}

impl From<ParseIntError> for CustomError {
    fn from(err: ParseIntError) -> CustomError {
        CustomError::Parse(err)
    }
}

fn first_element(vec: Vec<&str>) -> Result<i32> {
    let first = vec.first().ok_or(CustomError::EmptyVec)?;
    let element = first.parse::<i32>()?;

    Ok(element)
}

fn print(result: Result<i32>) {
    match result {
        Ok(n) => println!("The first element is {}", n),
        Err(e) => {
            println!("Error: {}", e);
            if let Some(source) = e.source() {
                println!("  Caused by: {}", source);
            }
        }
    }
}

fn main() {
    let numbers = vec!["1", "2", "3"];
    let empty = vec![];
    let strings = vec!["string", "2", "3"];

    print(first_element(numbers));
    print(first_element(empty));
    print(first_element(strings));
}

上記コードを実行すると下記のように、表示されます。

$ cargo run
The first element is 1
Error: list not empty
Error: invalid list element
  Caused by: invalid digit found in string

ただし、ライブラリがどんどん大きくなってきて、エラー型が増えていくとそれにともない、コード量も多くなっていき、見通しが悪くなっていきます。
そこでthiserror crateを使い、これらのコードの記載を簡略化していきます。

thiserrorでエラー処理を実装する

thiserrorは標準ライブラリーのstd::error::Errorトレイを実装するための便利なマクロを提供します。
cargo.tomlにthiserrorを記載して、上記コードを修正します。

Cargo.toml
[dependencies]
thiserror= "1"
main.rs

 use std::error::Error as StdError;
-use std::fmt;
 use std::num::ParseIntError;
+use thiserror::Error;

 type Result<T> = std::result::Result<T, CustomError>;

- #[derive(Debug)]
+ #[derive(Debug, Error)]
  enum CustomError {
+     #[error("list not empty")]
      EmptyVec,
+     #[error("invalid list element")]
-     Parse(ParseIntError),
+     Parse(#[from] ParseIntError),
  }
- 
- impl fmt::Display for CustomError {
-     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-         match *self {
-             CustomError::EmptyVec => write!(f, "list not empty"),
-             CustomError::Parse(_) => write!(f, "invalid list element"),
-         }
-     }
- }
- 
- impl StdError for CustomError {
-     fn source(&self) -> Option<&(dyn StdError + 'static)> {
-         match *self {
-             CustomError::EmptyVec => None,
-             CustomError::Parse(ref e) => Some(e),
-         }
-     }
- }
- 
- impl From<ParseIntError> for CustomError {
-     fn from(err: ParseIntError) -> CustomError {
-         CustomError::Parse(err)
-     }
- }

fn first_element(vec: Vec<&str>) -> Result<i32> {
    let first = vec.first().ok_or(CustomError::EmptyVec)?;
    let element = first.parse::<i32>()?;

    Ok(element)
}

fn print(result: Result<i32>) {
    match result {
        Ok(n) => println!("The first element is {}", n),
        Err(e) => {
            println!("Error: {}", e);
            if let Some(source) = e.source() {
                println!("  Caused by: {}", source);
            }
        }
    }
}

fn main() {
    let numbers = vec!["1", "2", "3"];
    let empty = vec![];
    let strings = vec!["string", "2", "3"];

    print(first_element(numbers));
    print(first_element(empty));
    print(first_element(strings));
}

このように記載することで最初のコードと同じように出力できます。

$ cargo run
The first element is 1
Error: list not empty
Error: invalid list element
  Caused by: invalid digit found in string

thiserrorによりDisplay, Error, From implの実装を自動でおこなってくれます。
簡単に説明すると、#[error("..")]()内のメッセージを記載すると、Display implを実装してメッセージが表示してくれます。
また#[error("{var}")]ように書くとDisplay impl内のwrite!("{}", self.var)のように動的にメッセージを表示することもできます。
さらに#[from]でFrom implとsource()の実装を自動でしてくれます。
その他にも機能やコードがあるので興味がある方はthiserrorのDocumentを参照してみてください。
https://docs.rs/thiserror/latest/thiserror/

Discussion