| Crates.io | snafu-virtstack-macro |
| lib.rs | snafu-virtstack-macro |
| version | 0.1.0 |
| created_at | 2025-08-06 16:00:43.867943+00 |
| updated_at | 2025-08-06 16:00:43.867943+00 |
| description | Proc macro for snafu-virtstack providing automatic virtual stack trace implementation |
| homepage | https://github.com/wadtech/snafu-virtstack |
| repository | https://github.com/wadtech/snafu-virtstack |
| max_upload_size | |
| id | 1783957 |
| size | 19,939 |
A lightweight, efficient error handling library for Rust that implements virtual stack traces based on GreptimeDB's error handling approach. This library combines the power of SNAFU error handling with virtual stack traces to provide meaningful error context without the overhead of system backtraces.
Traditional error handling in Rust often faces a dilemma:
Virtual stack traces provide a third way: capturing meaningful context at each error propagation point with minimal overhead.
Add these dependencies to your Cargo.toml:
[dependencies]
snafu = "0.8"
snafu-virtstack = "0.1"
The virtual stack trace approach provides several key advantages:
Unlike system backtraces that capture the entire call stack (expensive operation), virtual stack traces only record error propagation points. This results in:
Virtual stack traces capture:
use snafu::prelude::*;
use snafu_virtstack::stack_trace_debug;
#[derive(Snafu)]
#[stack_trace_debug]
pub enum AppError {
#[snafu(display("Failed to read configuration file"))]
ConfigRead { source: std::io::Error },
#[snafu(display("Invalid configuration format"))]
ConfigParse { source: serde_json::Error },
#[snafu(display("Database connection failed"))]
DatabaseConnection { source: DatabaseError },
}
#[derive(Snafu)]
#[stack_trace_debug]
pub enum DatabaseError {
#[snafu(display("Connection timeout"))]
Timeout,
#[snafu(display("Invalid credentials"))]
InvalidCredentials,
}
fn read_config() -> Result<Config, AppError> {
let content = std::fs::read_to_string("config.json")
.context(ConfigReadSnafu)?;
let config: Config = serde_json::from_str(&content)
.context(ConfigParseSnafu)?;
Ok(config)
}
// When an error occurs, you get a detailed virtual stack trace:
// Error: Failed to read configuration file
// Virtual Stack Trace:
// 0: Failed to read configuration file at src/main.rs:45:10
// 1: No such file or directory (os error 2) at src/main.rs:46:15
use snafu::prelude::*;
use snafu_virtstack::{stack_trace_debug, VirtualStackTrace};
#[derive(Snafu)]
#[stack_trace_debug]
pub enum ServiceError {
#[snafu(display("User {id} not found"))]
UserNotFound { id: u64 },
#[snafu(display("Failed to query database"))]
DatabaseQuery { source: DatabaseError },
#[snafu(display("Failed to serialize response"))]
Serialization { source: serde_json::Error },
#[snafu(display("Request validation failed: {reason}"))]
ValidationFailed { reason: String },
}
#[derive(Snafu)]
#[stack_trace_debug]
pub enum DatabaseError {
#[snafu(display("Query execution failed"))]
QueryExecution { source: sqlx::Error },
#[snafu(display("Connection pool exhausted"))]
PoolExhausted,
#[snafu(display("Transaction rolled back"))]
TransactionRollback,
}
pub struct UserService {
db: Database,
}
impl UserService {
pub async fn get_user(&self, id: u64) -> Result<User, ServiceError> {
// Validation with custom error context
ensure!(id > 0, ValidationFailedSnafu {
reason: "User ID must be positive"
});
// Database query with error propagation
let user = self.db
.query_user(id)
.await
.context(DatabaseQuerySnafu)?;
// Check if user exists
let user = user.ok_or_else(|| ServiceError::UserNotFound { id })?;
Ok(user)
}
pub async fn get_user_json(&self, id: u64) -> Result<String, ServiceError> {
let user = self.get_user(id).await?;
// Serialize with error context
serde_json::to_string(&user)
.context(SerializationSnafu)
}
}
// Error handling in practice
async fn handle_request(service: &UserService, id: u64) {
match service.get_user_json(id).await {
Ok(json) => println!("Success: {}", json),
Err(e) => {
// For developers: Full virtual stack trace
eprintln!("{:?}", e);
// For users: Simple error message
eprintln!("Error: {}", e);
// Access the virtual stack programmatically
let stack = e.virtual_stack();
for (i, frame) in stack.iter().enumerate() {
eprintln!("Frame {}: {} at {}:{}",
i,
frame.message,
frame.location.file(),
frame.location.line()
);
}
}
}
}
When working with errors that need to be boxed (e.g., when using Box<dyn std::error::Error + Send + Sync>), you can use the .boxed() method to convert errors before applying context:
use snafu::prelude::*;
use snafu_virtstack::stack_trace_debug;
#[derive(Snafu)]
#[stack_trace_debug]
pub enum MyError {
#[snafu(display("Filesystem IO issue: {source}"))]
FilesystemIoFailure { source: Box<dyn std::error::Error + Send + Sync> },
#[snafu(display("Configuration error: {source}"))]
ConfigurationError { source: Box<dyn std::error::Error + Send + Sync> },
}
fn read_config() -> Result<String, MyError> {
// Box the error before applying context
let content = std::fs::read_to_string("config.toml")
.boxed()
.context(FilesystemIoFailureSnafu)?;
// Parse with another boxed error
let parsed: Config = toml::from_str(&content)
.boxed()
.context(ConfigurationErrorSnafu)?;
Ok(format!("{:?}", parsed))
}
The .boxed() method is particularly useful when:
Send + SyncDO use #[stack_trace_debug] on all error enums
#[derive(Snafu)]
#[stack_trace_debug] // Always add this
enum MyError { ... }
DO provide meaningful error messages
#[snafu(display("Failed to process user {id}: {reason}"))]
ProcessingFailed { id: u64, reason: String },
DO use context when propagating errors
operation()
.context(OperationFailedSnafu { context: "important detail" })?;
DO separate internal and external errors
// Internal error with full details
#[derive(Snafu)]
#[stack_trace_debug]
enum InternalError { ... }
// External error for API responses
#[derive(Snafu)]
enum ApiError { ... }
DO leverage the error chain
// Each level adds context
low_level_op()
.context(LowLevelSnafu)?
.middle_layer()
.context(MiddleLayerSnafu)?
.high_level()
.context(HighLevelSnafu)?;
DO use ensure! for validation
ensure!(value > 0, InvalidValueSnafu { value });
DON'T use system backtraces in production
// Avoid this in production
std::env::set_var("RUST_BACKTRACE", "1");
DON'T ignore error context
// Bad: loses context
operation().map_err(|_| MyError::Generic)?;
// Good: preserves context
operation().context(SpecificSnafu)?;
DON'T create deeply nested error types
// Bad: too many levels
enum Error1 { E2(Error2) }
enum Error2 { E3(Error3) }
enum Error3 { E4(Error4) }
// Good: flat structure with sources
enum AppError {
Database { source: DbError },
Network { source: NetError },
}
DON'T expose internal errors directly to users
// Bad: exposes implementation details
return Err(internal_error);
// Good: map to user-friendly error
return Err(map_to_external_error(internal_error));
DON'T forget to handle all error variants
// Use exhaustive matching
match error {
Error::Variant1 => handle_1(),
Error::Variant2 => handle_2(),
// Don't use _ => {} unless necessary
}
DON'T mix error handling strategies
// Pick one approach and stick to it
// Either use SNAFU throughout or another error library
// Don't mix multiple error handling crates
Proc Macro Magic: The #[stack_trace_debug] attribute automatically implements:
VirtualStackTrace trait for stack frame collectionDebug implementation for formatted outputLocation Tracking: Uses Rust's #[track_caller] to capture precise locations where errors are propagated
Error Chain Walking: Automatically traverses the source() chain to build complete error context
Zero-Cost Until Needed: Stack frames are only generated when the error is actually inspected
VirtualStackTracepub trait VirtualStackTrace {
fn virtual_stack(&self) -> Vec<StackFrame>;
}
StackFramepub struct StackFrame {
pub location: &'static std::panic::Location<'static>,
pub message: String,
}
#[stack_trace_debug]Attribute macro that automatically implements virtual stack trace functionality for SNAFU error enums.
Contributions are welcome! Please feel free to submit issues and pull requests.
This project is licensed under the Apache 2.0 License - see the LICENSE file for details.