This page has been machine-translated from the original page.
This time, I will briefly summarize what Rust’s thiserror crate is and how to use it.
Reference: thiserror - Rust
Table of Contents
- What Is the thiserror Crate?
- Error Handling in Rust
- Using thiserror for Error Handling
- Minimal Example Using thiserror
- Summary
What Is the thiserror Crate?
thiserror is a crate (a library in Rust) for concisely implementing the Rust std::error::Error trait (which defines common methods across different types) with derive macros (which automatically generate trait implementation code for structs and enums).
anyhow is another useful crate for error handling in Rust, but compared with anyhow, thiserror has advantages when implementing custom error types. It is commonly used in libraries where error types need to be explicitly exposed so callers can distinguish them.
Reference: thiserror vs anyhow, the error-handling holy war #Rust - Qiita
Error Handling in Rust
To begin with, Rust does not have try-catch style error handling, and errors are represented with the Result<T, E> type.
enum Result<T, E> {
Ok(T), // Value on success
Err(E), // Value on error
}Reference: Rust Is Great: Error Handling Edition
In Rust, the Result type is used as the result of operations that may fail.
The following function is a straightforward implementation of error handling when using the read_to_string function.
Rust can use pattern matching with match like this to perform error handling based on the Result return value.
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(())
}Reference: match - Rust By Example
In Rust, using the ? operator lets you write nearly the same Result handling as the match expression above, as shown below.
use std::fs;
fn main() -> Result<(), std::io::Error> {
let content = fs::read_to_string("test.txt")?;
println!("{}", content);
Ok(())
}Reference: Recoverable Errors with Result - The Rust Programming Language (Japanese Edition)
Reference: Rust’s ? Operator #Rust - Qiita
Also, similar processing can be implemented like this.
use std::fs;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let message: String = fs::read_to_string("message.txt")?;
println!("{}", message);
Ok(())
}In the code above, errors are handled using Box<dyn std::error::Error>.
Rust requires a function’s return type to be clear at compile time, but here dyn std::error::Error dynamically dispatches some type that implements the std::error::Error trait.
In such cases, Box is used to point to data stored on the heap.
This makes it possible to use a type even when its size cannot be determined at compile time, and it also lets you receive errors even when different error types may be returned inside the function, as shown below.
use std::fs;
fn main() -> Result<(), Box<dyn std::error::Error>> {
// io::Error is automatically converted to Box<dyn Error> by ?
// let content = fs::read_to_string("Cargo.toml")?;
let content = fs::read_to_string("xxx.toml")?;
// ParseIntError can also be converted to the same Box<dyn Error>
let port: u16 = content.trim().parse()?;
println!("port = {}", port);
Ok(())
}Reference: Returning Traits with dyn - Rust By Example (Japanese Edition)
Reference: Using Box
Using thiserror for Error Handling
When the caller handles errors, using Box<dyn std::error::Error> makes it easy to handle multiple types, but it also has drawbacks, such as making it harder for the caller to determine the error kind.
thiserror makes it easy to implement explicit custom error types, so it is mainly used in library implementations that need to expose error types structurally and allow callers to pattern match on them.
Minimal Example Using thiserror
Below is a simple error-handling implementation using thiserror. Here, an enum defines an error called AppError.
The leading #[derive(Debug, Error)] is used to automatically implement the std::error::Error trait.
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),
}
}In the code above, #[from] automatically generates the implementation that converts std::io::Error into AppError, and when errors are propagated with the ? operator, conversion from io::Error to AppError is performed automatically.
Summary
I may add more implementation patterns that use thiserror later.