Choosing the Right Rust Error Handling Tool: anyhow, thiserror, or snafu?
Daniel Hayes
Full-Stack Engineer · Leapcell

Error handling is an indispensable part of Rust development. Rust’s Result<T, E>
provides fundamental support, but the specific implementation varies depending on the scenario. This article introduces three commonly used error handling tools—anyhow
, thiserror
, and snafu
—analyzing their characteristics and applicable scenarios, and helps you understand through practical examples how to use them in projects. Whether you are developing applications or writing libraries, this article can serve as a valuable reference.
This article delves deeply into the three major tools for Rust error handling:
anyhow
is suitable for rapid and unified error handling, ideal for application development;thiserror
supports customized error types, making it suitable for library development;snafu
provides context-driven error management, appropriate for complex systems.
Through comparing their strengths and weaknesses and demonstrating actual code examples, we will show you how to choose the right tool based on project needs. We also provide project setup steps and example code to help you handle errors more effectively in Rust.
Rust Error Handling
Error Handling: anyhow
, thiserror
, snafu
anyhow
: unified and simple error handling, suitable for application-level programmingthiserror
: customized and rich error handling, suitable for library-level programmingsnafu
: finer-grained error management
Note: When developing, pay attention to the size of Result<T, E>
.
anyhow
Error: Application-level Error Handling
Conversion and unified error handling with anyhow::Error
:
- Provides a unified
anyhow::Error
type, supporting any error type that implementsstd::error::Error
- Uses the
?
operator for automatic error propagation, simplifying multi-layer nested error handling - Supports adding dynamic context (via the
context()
method) to enhance error readability
fn get_cluster_info() -> Result<ClusterMap, anyhow::Error> { // Error 3: Err3 let config = std::fs::read_to_string("cluster.json")?; // Error 1: Err1 // let config = std::fs::read_to_string("cluster.json").context("...")?; // Error 1: Err1 let map: ClusterMap = serde_json::from_str(&config)?; // Error 2: Err2 Ok(map) } struct Err1 {...} struct Err2 {...} match ret { Ok(v) => v, Err(e) => return Err(e.into()) } Err1 => Err3: impl From<Err1> for Err3 Err2 => Err3: impl From<Err2> for Err3 impl From<Err1> for Err3 { fn from(v: Err1) -> Err3 { ... } }
thiserror
Error: Defining Library-level Errors
- Automatically generates error types that conform to
std::error::Error
via macros - Supports nested error sources (using the
#[from]
attribute) and structured error information - Allows customization of error message templates (e.g.,
#[error("Invalid header: {expected}")]
)
Reference: Rust std::error::Error trait documentation
Since the Error
trait requires implementing both Debug
and Display
:
pub trait Error: Debug + Display {
It can be printed like this:
Error -> println!("{}/ {:?}", err)
snafu
Error: Context-driven Error Management
- Converts underlying errors into domain-specific errors through the
Snafu
macro - Supports attaching structured context (such as file paths, input parameters) in the error chain
- Provides the
ensure!
macro to simplify condition checking and error throwing
thiserror
vs snafu
For more information, refer to: kube-rs/kube discussion #453
Comparison and Selection Guide
Dimension | anyhow | thiserror | snafu |
---|---|---|---|
Error Type | Unified dynamic type | Static custom type | Domain-driven type |
Context Support | Dynamic string | Struct fields | Structured fields + dynamic templates |
Suitable Stage | Application development (rapid iteration) | Library development (stable interfaces) | Complex systems (maintainability) |
Learning Curve | Low (no need to predefine types) | Medium (requires designing error structures) | High (requires understanding context models) |
Typical Users | Frontend developers / scripting tool developers | Framework developers | Infrastructure engineers |
Practical Implementation
Create and Initialize Project rust-ecosystem-learning
Based on a Template
cargo generate --git git@github.com:qiaopengjun5162/rust-template.git cd rust-ecosystem-learning code .
Project Directory Structure
rust-ecosystem-learning on main [!] is 📦 0.1.0 via 🦀 1.85.0 via 🅒 base ➜ tree . -L 6 -I 'target|coverage|coverage_report|node_modules' . ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── _typos.toml ├── cliff.toml ├── deny.toml ├── docs └── src ├── error.rs ├── lib.rs └── main.rs 3 directories, 12 files
Add Dependencies
cargo add anyhow cargo add thiserror cargo add serde_json
main.rs
File
use anyhow::Context; use rust_ecosystem_learning::MyError; use std::fs; use std::mem::size_of; fn main() -> Result<(), anyhow::Error> { println!("size of anyhow::Error: {}", size_of::<anyhow::Error>()); println!("size of std::io::Error: {}", size_of::<std::io::Error>()); println!( "size of std::num::ParseIntError: {}", size_of::<std::num::ParseIntError>() ); println!( "size of serde_json::Error: {}", size_of::<serde_json::Error>() ); println!("size of string: {}", size_of::<String>()); println!("size of MyError: {}", size_of::<MyError>()); let filename = "non_existent_file.txt"; let _fd = fs::File::open(filename).with_context(|| format!("Cannot find file: {}", filename))?; fail_with_error()?; Ok(()) } fn fail_with_error() -> Result<(), MyError> { Err(MyError::Custom("This is a custom error".to_string())) } #[cfg(test)] mod tests { #[test] fn it_works() { assert_eq!(2 + 2, 4); } }
lib.rs
File
mod error; pub use error::MyError;
error.rs
File
use thiserror::Error; #[derive(Error, Debug)] pub enum MyError { #[error("IO error: {0}")] Io(#[from] std::io::Error), #[error("Parse error: {0}")] Parse(#[from] std::num::ParseIntError), #[error("Serialize JSON error: {0}")] Serialize(#[from] serde_json::Error), // #[error("Error: {a}, {b:?}, {c:?}, {d:?}")] // BigError { // a: String, // b: Vec<String>, // c: [u8; 64], // d: u64, // }, #[error("Error: {0:?}")] BigError(Box<BigError>), #[error("An error occurred: {0}")] Custom(String), } #[derive(Debug)] pub struct BigError { pub a: String, pub b: Vec<String>, pub c: [u8; 64], pub d: u64, }
Cargo.toml
File
[package] name = "rust-ecosystem-learning" version = "0.1.0" edition = "2021" license = "MIT" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] anyhow = "1.0.97" serde = { version = "1.0.217", features = ["derive"] } serde_json = "1.0.140" thiserror = "2.0.11"
Running the Project
rust-ecosystem-learning on main [!] is 📦 0.1.0 via 🦀 1.85.0 via 🅒 base took 2.9s ➜ cargo run Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.02s Running `target/debug/rust-ecosystem-learning` size of anyhow::Error: 8 size of std::io::Error: 8 size of std::num::ParseIntError: 1 size of serde_json::Error: 8 size of string: 24 size of MyError: 24 Error: Cannot find file: non_existent_file.txt Caused by: No such file or directory (os error 2)
Summary
Rust’s error handling tools each have their own focus:
anyhow
is simple and efficient, suitable for application development.thiserror
is structured and clear, suitable for library design.snafu
offers rich context, making it ideal for complex scenarios.
Through the analysis and practical examples in this article, you can choose the appropriate error handling solution based on your actual needs.
Proper error handling makes your code more robust — practice it and elevate the quality of your Rust projects!
We are Leapcell, your top choice for hosting Rust projects.
Leapcell is the Next-Gen Serverless Platform for Web Hosting, Async Tasks, and Redis:
Multi-Language Support
- Develop with Node.js, Python, Go, or Rust.
Deploy unlimited projects for free
- pay only for usage — no requests, no charges.
Unbeatable Cost Efficiency
- Pay-as-you-go with no idle charges.
- Example: $25 supports 6.94M requests at a 60ms average response time.
Streamlined Developer Experience
- Intuitive UI for effortless setup.
- Fully automated CI/CD pipelines and GitOps integration.
- Real-time metrics and logging for actionable insights.
Effortless Scalability and High Performance
- Auto-scaling to handle high concurrency with ease.
- Zero operational overhead — just focus on building.
Explore more in the Documentation!
Follow us on X: @LeapcellHQ