【Rust】thiserrorにより独自のエラー型を実装する方法

Rust で thiserror を用いて独自のエラー型を実装する方法について、初心者にも分かりやすく解説します。
thiserror クレートの概要
thiserror とは
Rust で独自のエラー型を定義する場合、std::error::Error トレイトや std::fmt::Display トレイトの実装が必要となり、エラーの種別が増えるほどボイラープレートコードという定型的なコードが増えていってしまいます。
thiserror は、独自のエラー型の実装を簡潔に書けるようにする便利なクレートです。
この記事では、thiserror を使った独自エラー型の定義方法の基本について紹介します。
thiserror の特徴(derive でエラー型の実装を簡素化する)
thiserror を使用する上で最低限理解したい内容としては以下です。
#[derive(Error)]でstd::error::Errorを実装でき、#[error("...")]に基づいてstd::fmt::Displayも自動生成される。#[from]により、エラー変換(From)を自動実装できる。#[error(transparent)]により、薄いラッパー型を簡潔に定義できる。
以降では、上記について例を用いて紹介していきます。
thiserror の位置づけ(Result<T, E> や anyhow との関係性)
Rust では、Result<T, E> という結果型を用いた明示的なエラー処理が基本です。T や E は、型パラメータであり、任意の型とすることができます。thiserror は、この E に使う「独自のエラー型」を定義しやすくするためのクレートです。
また、Rust のエラー処理では、anyhow というクレートもよく聞きます。各種クレートなどの様々な機能を使用する場合、それらの機能が独自に設計しているエラー型を扱う場合があります。この時、全てのエラー型を意識して実装するのは大変です。anyhow は、様々なエラー型をまとめて取り扱うことができます。
thiserror と anyhow の位置づけは以下のように理解しておいてもらうといいかと思います。
thiserror:独自のエラー型を簡単に定義する。anyhow:様々なエラー型をまとめて取り扱うことができる。
一般的には、ライブラリ側では thiserror、アプリケーション側では anyhow が使われることが多いです。
thiserror の基本
thiserror の導入方法
thiserror は他クレート同様に cargo add または Cargo.toml に追記することで利用できるようになります。
【cargo add で追加する場合】
cargo add thiserror
【Cargo.toml に追記する場合】
[dependencies] thiserror = "2.0.18"
例での記載バージョンは、記事執筆時点での最新バージョンで記載しているため、導入時には crates.io の thiserror のページを確認して指定してください。
独自エラー型の定義方法(基本)
thiserror の基本的な使い方を以下の例で見ていきましょう。以下の例では、独自の MyError というエラー型を作成しています。
use thiserror::Error;
// 独自エラー型の定義
#[derive(Error, Debug)]
enum MyError {
#[error("不正な入力です。")]
InvalidInput,
#[error("権限がありません。")]
PermissionDenied,
}
fn main() {
// 不正な入力エラー
let err = MyError::InvalidInput;
println!("{err}");
println!("{err:?}");
// 権限拒否エラー
let err = MyError::PermissionDenied;
println!("{err}");
println!("{err:?}");
}【実行結果】 不正な入力です。 InvalidInput 権限がありません。 PermissionDenied
上記例の独自エラー型は列挙型(enum)で定義しており、不正な入力を意味する InvalidInput と権限がないことを意味する PermissionDenied というバリアントを持っている型です。
thiserror を使用するには、まず「use thiserror::Error」をスコープに取り込みます。独自のエラー型に #[derive(Error, Debug)] をつけることで独自の型に各種トレイトを自動実装しています。
#[derive(Error)] により、std::error::Error トレイトが実装され、各バリアントに記載された #[error("...")] のエラーメッセージに基づいて std::fmt::Display トレイトの自動実装も行われています。また、#[derive(Debug)] では、std::fmt::Debug トレイトの自動実装が行われています。
main 関数では、この独自エラー型を生成して表示してみていますが、Display トレイトが実装されることで、独自に作ったエラー表示ができていることが分かるかと思います。なお、Debug トレイトの自動実装では、バリアント名や保持している値が表示されるようになります。
引数によりエラーに情報を持たせる
thiserror を使って構築するエラー型では、エラーの原因を調査しやすくするために、エラーに関連するような情報を引数として渡して受け取ることができます。
use thiserror::Error;
// 独自エラー型の定義 引数付き
#[derive(Error, Debug)]
enum MyError {
#[error("不正な入力です: {0}")]
InvalidInput(String),
#[error("権限がありません: ID {user_id}")]
PermissionDenied { user_id: u32 },
}
fn main() {
// 不正な入力エラー
let err = MyError::InvalidInput("空の文字列".to_string());
println!("{err}");
println!("{err:?}");
// 権限拒否エラー
let err = MyError::PermissionDenied { user_id: 42 };
println!("{err}");
println!("{err:?}");
}【実行結果】
不正な入力です: 空の文字列
InvalidInput("空の文字列")
権限がありません: ID 42
PermissionDenied { user_id: 42 }例では、#[error("...")] 内で指定する文字列に、引数を埋め込むように指定することができます。引数の指定については、値を直接持たせる形式(tuple variant)とフィールド名付きの形式(struct variant)の大きく 2 種類があります。
値を直接持たせる形式(tuple variant)の場合には、エラー文字列の埋め込む位置に {0}, {1}, … のように記載し、引数として渡された順に値が埋め込まれます。複数の引数を渡す場合には「,」で列挙して渡します。
フィールド名付きの形式(struct variant)の場合には、エラー文字列の埋め込む位置に {user_id} のようにフィールド名を記載します。引数を渡す際には { user_id: 42 } のようにフィールドと値を設定して渡します。
#[from] によるエラー変換
独自のエラー型は、ある関数やメソッドの返却値として使用します。当該関数やメソッドで下位のモジュールを使用する場合、下位からもエラーが返却されることがあります。この場合、エラーをその場で処理せずに上位へ伝播する際に Rust では「?」をよく使用します。
この時に、下位のエラーを独自のエラー型に変換して伝播したい場合は、#[from] により独自エラー型へ型変換すると便利です。具体的には「impl From<std::io::Error> for MyError」が自動生成されます。
use std::fs;
use thiserror::Error;
#[derive(Error, Debug)]
enum MyError {
// std::io::Error から自動的に変換されるエラー
#[error("My I/Oエラーが発生: {0}")]
Io(#[from] std::io::Error),
}
// ファイルを読み込む関数
fn my_read_file(path: &str) -> Result<String, MyError> {
// fs::read_to_string は std::io::Error を返却するが、
// #[from] により MyError に自動変換される
let content = fs::read_to_string(path)?;
Ok(content)
}
fn main() {
match my_read_file("non_existent_file.txt") {
Ok(content) => println!("ファイル内容: {content}"),
Err(e) => {
println!("エラー : {e}");
println!("デバッグ情報: {e:?}");
}
}
}【実行結果】
エラー : My I/Oエラーが発生: 指定されたファイルが見つかりません。 (os error 2)
デバッグ情報: Io(Os { code: 2, kind: NotFound, message: "指定されたファイルが見つかりません。" })例では、my_read_file という関数でファイルを読み込もうとしていますが、ファイルが開けない場合などでは、std::io::Error が返却されてきます。
このエラーを独自エラー型に変換する場合は、「Io(#[from] std::io::Error)」の部分のように記載します。これは、Io バリアントに std::io::Error を詰めていることを表していて、#[error("...")] 内の {0} の部分には、std::io::Error のエラー表示が埋め込まれます。
このようにして、下位エラーを MyError に変換し、Result<T, MyError> の Err(...) として返却できるようになります。
#[error(transparent)] により透過的にエラーを扱う
上記の #[from] の例では、下位のモジュールのエラーを独自エラー型に変換しました。場合によっては、下位のモジュールのエラー内容をそのまま利用するだけで十分な場合があります。そのような場合は、以下のように「#[error(transparent)]」を使用するのが便利です。
use std::fs;
use thiserror::Error;
#[derive(Error, Debug)]
enum MyError {
// 下位のエラーをそのまま伝搬させる
#[error(transparent)]
Io(#[from] std::io::Error),
}
// ファイルを読み込む関数
fn my_read_file(path: &str) -> Result<String, MyError> {
let content = fs::read_to_string(path)?;
Ok(content)
}
fn main() {
match my_read_file("non_existent_file.txt") {
Ok(content) => println!("ファイル内容: {content}"),
Err(e) => {
println!("エラー : {e}");
println!("デバッグ情報: {e:?}");
}
}
}【実行結果】
エラー : 指定されたファイルが見つかりません。 (os error 2)
デバッグ情報: Io(Os { code: 2, kind: NotFound, message: "指定されたファイルが見つかりません。" })transparent とは「透明」という意味で、独自のエラーなのに下位のエラーが透過的にみえるというイメージを持ってもらうと分かりやすいかと思います。薄いラッパーのエラー型を作りたい場合には、非常によく使われる方法です。
まとめ
この記事では、Rust で thiserror を用いて独自のエラー型を実装する方法について、初心者向けに解説しました。
thiserror を使用することで独自エラー型を実装する際に発生しがちなボイラープレートコードを削減しつつ、Result<T, E> の E に適したエラー型を自然な形で設計できます。
thiserror の入門として最低限押さえておきたいポイントは以下の通りです。
#[derive(Error)]でstd::error::Errorを実装でき、#[error("...")]に基づいてstd::fmt::Displayも自動生成される。#[from]により、エラー変換(From)を自動実装できる。#[error(transparent)]により、薄いラッパー型を簡潔に定義できる。
また、エラー処理関連でよく紹介される anyhow については、一般的に以下のような使い分けがされます。
- ライブラリ/モジュール側:
thiserrorによる独自エラー型を定義 - アプリケーション側:
anyhowによる複数エラーの統一的な取り扱い
anyhow については、また別の記事で改めて整理して紹介します。
ぜひ、thiserror をうまく活用して、扱いやすい独自エラー型を実装してみてください。

