If this is useful, a ❤️ helps others find it.
All tests run on an 8-year-old MacBook Air.
Every Rust tutorial covers Result and ?. Few cover what to actually do when you have 5 different error types flying around a real application.
Here's what I settled on after shipping multiple Tauri apps.
The problem: heterogeneous errors
A PDF processing command might fail due to:
- IO error (file not found)
- lopdf parse error (malformed PDF)
- Encryption error (wrong key)
- Swift sidecar error (process failed)
- Serialization error (JSON parse)
Returning Box works but loses type information. Returning String works but is unstructured. Neither is great.
Pattern 1: domain error enum
Define one error type per domain:
#[derive(Debug, thiserror::Error)]
pub enum PdfError {
#[error("file not found: {path}")]
NotFound { path: String },
#[error("malformed PDF: {reason}")]
ParseError { reason: String },
#[error("decryption failed — wrong password or corrupted file")]
DecryptionFailed,
#[error("sidecar process failed: {stderr}")]
SidecarError { stderr: String },
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
}
thiserror generates the Display and Error impls. #[from] generates From automatically — ? just works.
Pattern 2: convert at the boundary
Tauri commands need Result for the frontend. Convert at the command boundary, not inside business logic:
// Business logic — rich error types
pub fn process_pdf(path: &str) -> Result {
// ...
}
// Tauri command — converts at boundary
#[tauri::command]
pub fn process(path: String) -> Result {
process_pdf(&path).map_err(|e| e.to_string())
}
The frontend gets a human-readable error string. The backend keeps structured errors for matching and logging.
Pattern 3: context with anyhow
For scripts and one-off tools where rich error types aren't worth the boilerplate, anyhow adds context without ceremony:
use anyhow::{Context, Result};
pub fn load_config(path: &str) -> Result {
let content = std::fs::read_to_string(path)
.with_context(|| format!("failed to read config from {}", path))?;
serde_json::from_str(&content)
.with_context(|| "config file is not valid JSON")
}
Error messages chain automatically: "config file is not valid JSON: unexpected character at line 3"
Pattern 4: never panic in library code
unwrap() and expect() in library functions crash the whole app. In a long-running desktop app, a crash from a malformed PDF file is unacceptable.
Rule: unwrap() only in main() for setup that must succeed, or in tests.
// Bad — panics if PDF is malformed
let doc = Document::load(path).unwrap();
// Good — surfaces error to caller
let doc = Document::load(path)
.map_err(|e| PdfError::ParseError { reason: e.to_string() })?;
What I actually use
-
thiserrorfor domain error enums in library code -
anyhowfor scripts and one-off tools -
.map_err(|e| e.to_string())at Tauri command boundaries - Never
unwrap()in non-test, non-main code
Hiyoko PDF Vault → https://hiyokoko.gumroad.com/l/HiyokoPDFVault
X → @hiyoyok
Top comments (0)