Rustで独自のエラータイプの実装を楽にするthiserrorの使い方

Rustで独自のエラータイプを実装する必要がある場合、dtolnay/thiserror を使うことで簡単にカスタムエラータイプを実装できます。

この記事では、thiserrorを「利用した実装」と「利用していない実装」を比べることで、どれだけ簡単にカスタムエラータイプを作れるか説明していきます。ちなみに、thiserrorの詳細までは説明しないので、知りたい方はREADMEをご覧ください。

thiserrorの概要

はじめにthiserrorの特徴について簡単に説明します。

  • thiserrorは標準ライブラリの std::error::Errorトレイトの便利なderiveマクロを提供している
  • カスタムエラータイプを実装する際のfmt::Displaystd::error::ErrorFrom<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::Displaystd::error::ErrorFrom<T>トレイトの実装をほぼ省略できます。ぜひ、使ってみてください。

ここで紹介した以外の便利な機能もあるので、詳細を知りたい方はdtolnay/thiserror のREADMEを見てください。

参考