今回は、Rust の thiserror crate の概要や利用方法などについて簡単にまとめます。
もくじ
thiserror crate とは
thiserror は、Rust の std::error::Error トレイト(異なる型に対して共通のメソッドを定義したもの)を derive マクロ(struct や enum を使用してトレイトのコードを自動的に生成するもの)で簡潔に実装するためのクレート(Rust におけるライブラリ)です。
Rust のエラーハンドリングに有効なクレートとしては anyhow がありますが、thiserror は anyhow と比較してカスタムエラー型の実装などに利点があり、主にエラーの型を明示的に公開し呼び出し元側で判別できるようにする必要があるライブラリの実装などで使用されることが一般的なようです。
参考:thiserror vs anyhow、エラーハンドリング宗教戦争 #Rust - Qiita
Rust におけるエラーハンドリング
前提として、Rust では try-catch によるエラーハンドリングは存在せず、エラーは Result<T, E> 型で表現されます。
enum Result<T, E> {
Ok(T), // 成功時の値
Err(E), // エラー時の値
}そして、Rust ではこの Result 型は「エラーが発生するかもしれない処理」の結果として使用されます。
以下の関数は、read_to_string 関数を使用する場合のエラーハンドリングを愚直に実装したものです。
Rust では、このような match によるパターンマッチングを使用して、Result 型の戻り値に基づくエラーハンドリングを行うことができます。
use std::fs;
fn main() -> Result<(), std::io::Error> {
let content = match fs::read_to_string("test.txt") {
Ok(text) => text,
Err(err) => return Err(err),
};
println!("{}", content);
Ok(())
}さらに Rust では ? 演算子を使用することでこの match 式を使用する場合とほぼ同等の Result 型に対する処理を以下のように実装することができます。
use std::fs;
fn main() -> Result<(), std::io::Error> {
let content = fs::read_to_string("test.txt")?;
println!("{}", content);
Ok(())
}参考:Resultで回復可能なエラー - The Rust Programming Language 日本語版
また、このコードと同じような処理は以下のように実装することもできます。
use std::fs;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let message: String = fs::read_to_string("message.txt")?;
println!("{}", message);
Ok(())
}上記のコードでは、Box<dyn std::error::Error> を使用してエラーを受け取ります。
Rust のコンパイル時には関数の戻り値の型が明確であることが要求されますが、ここでは dyn std::error::Error のように std::error::Error トレイトを実装する何らかの型を動的にディスパッチします。
このような場合は、ヒープに格納するデータを指す Box を使用します。
これにより、コンパイル時には型のサイズを特定できない場合でもその型を使用することができるようになり、以下のように関数内で異なる型のエラーが返される可能性がある場合でもそのエラーを受け取ることができるようになります。
use std::fs;
fn main() -> Result<(), Box<dyn std::error::Error>> {
// io::Error が ? で Box<dyn Error> に自動変換される
// let content = fs::read_to_string("Cargo.toml")?;
let content = fs::read_to_string("xxx.toml")?;
// ParseIntError も同じ Box<dyn Error> に変換できる
let port: u16 = content.trim().parse()?;
println!("port = {}", port);
Ok(())
}参考:dynを利用してトレイトを返す - Rust By Example 日本語版
参考:ヒープのデータを指すBox
エラーハンドリングに thiserror を使用する
呼び出し側でエラーハンドリングを行う場合、前述した Box<dyn std::error::Error> を使用すると手軽に複数の型を扱えるようになりますが、一方で呼び出し側ではエラーの種類を判断しにくいなどのデメリットがあります。
thiserror を使用すると、明示的なカスタムエラー型を簡単に実装することができるので、エラー型を構造的に公開し、呼び出し側でパターンマッチを可能とする必要があるようなライブラリの実装で主に使用されます。
thiserror を使用する最小サンプル
以下は、thiserror を使用したシンプルなエラーハンドリングの実装です。ここでは enum により AppError というエラーを定義しています。
先頭の #[derive(Debug, Error)] は std::error::Error トレイトの自動実装のための要件として記述しています。
use thiserror::Error;
#[derive(Debug, Error)]
pub enum AppError {
#[error("I/O エラー: {0}")]
Io(#[from] std::io::Error),
}
fn handle_error(err: &AppError) {
match err {
AppError::Io(e) => {
eprintln!("[IO] ファイル操作失敗: {}", e);
}
}
}
fn read_config(path: &str) -> Result<String, AppError> {
let content = std::fs::read_to_string(path)?;
Ok(content)
}
fn main() {
println!("--- 1. 存在しないファイルの読み込み (#[from] による自動変換) ---");
match read_config("nonexistent.toml") {
Ok(content) => println!("{}", content),
Err(e) => handle_error(&e),
}
println!("\n--- 2. 存在するファイルの読み込み (#[from] による自動変換) ---");
match read_config("Cargo.toml") {
Ok(content) => println!("{}", content),
Err(e) => handle_error(&e),
}
}上記のコードでは、#[from] により std::io::Error を AppError に変換する実装を自動生成しており、? 演算子でエラー委譲が行われた際に自動的に io::Error → AppError への変換が行われます。
まとめ
thiserror を使用する実装パターンについてはそのうち追記します。