snafu-virtstack 0.1.0

A lightweight, efficient error handling library for Rust that implements virtual stack traces based on GreptimeDB's error handling approach
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
# SNAFU Virtual Stack Trace

A lightweight, efficient error handling library for Rust that implements virtual stack traces based on [GreptimeDB's error handling approach](https://greptime.com/blogs/2024-05-07-error-rust). This library combines the power of [SNAFU](https://github.com/shepmaster/snafu) error handling with virtual stack traces to provide meaningful error context without the overhead of system backtraces.

## Table of Contents

- [Motivation]#motivation
- [Features]#features
- [Installation]#installation
- [Usage]#usage
  - [Benefits and Motivations]#benefits-and-motivations
  - [Basic Example]#basic-example
  - [Advanced Example]#advanced-example
- [Do's and Don'ts]#dos-and-donts
- [How It Works]#how-it-works
- [Performance]#performance
- [API Reference]#api-reference

## Motivation

Traditional error handling in Rust often faces a dilemma:
- **Option 1:** Use system backtraces - long hard to read stack traces only referencing functions and lines
- **Option 2:** Simple error propagation - lacks context about where errors originated

Virtual stack traces provide a third way: capturing meaningful context at each error propagation point with minimal overhead.

## Features

- 🚀 **Lightweight**: Only ~100KB binary overhead vs several MB for system backtraces
- 📍 **Precise Location Tracking**: Automatically captures file, line, and column information
- 🔗 **Error Chain Walking**: Traverses the entire error source chain
- 🎯 **Zero-Cost Abstraction**: Context generation can be postponed until needed
- 🛠️ **Seamless Integration**: Works perfectly with SNAFU error handling
- 📝 **Developer-Friendly**: Automatic Debug implementation with formatted stack traces

## Installation

Add these dependencies to your `Cargo.toml`:

```toml
[dependencies]
snafu = "0.8"
snafu-virtstack = "0.1"
```

## Usage

### Benefits and Motivations

The virtual stack trace approach provides several key advantages:

#### 1. **Performance Efficiency**
Unlike system backtraces that capture the entire call stack (expensive operation), virtual stack traces only record error propagation points. This results in:
- Lower CPU usage during error handling
- Reduced memory footprint
- Smaller binary sizes (100KB vs several MB)

#### 2. **Meaningful Context**
Virtual stack traces capture:
- The exact location where each error was propagated
- Custom error messages at each level
- The complete error chain from root cause to final error

#### 3. **Production-Ready**
- Safe to use in production environments
- No performance penalties in the happy path
- Can be enabled/disabled at runtime if needed

#### 4. **Developer Experience**
- Clear, readable error messages
- Easy to debug error propagation paths
- Automatic integration with existing SNAFU errors

#### 5. **Flexible Error Presentation**
- Detailed traces for developers during debugging
- Simplified messages for end users
- Customizable formatting for different contexts

### Basic Example

```rust
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
```

### Advanced Example

```rust
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()
                );
            }
        }
    }
}
```

### Boxing Errors

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:

```rust
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:
- You need to erase the specific error type
- Working with trait objects that require `Send + Sync`
- Dealing with multiple error types that need to be unified
- Creating library APIs that don't want to expose implementation details

## Do's and Don'ts

### ✅ Do's

1. **DO use `#[stack_trace_debug]` on all error enums**
   ```rust
   #[derive(Snafu)]
   #[stack_trace_debug]  // Always add this
   enum MyError { ... }
   ```

2. **DO provide meaningful error messages**
   ```rust
   #[snafu(display("Failed to process user {id}: {reason}"))]
   ProcessingFailed { id: u64, reason: String },
   ```

3. **DO use context when propagating errors**
   ```rust
   operation()
       .context(OperationFailedSnafu { context: "important detail" })?;
   ```

4. **DO separate internal and external errors**
   ```rust
   // Internal error with full details
   #[derive(Snafu)]
   #[stack_trace_debug]
   enum InternalError { ... }

   // External error for API responses
   #[derive(Snafu)]
   enum ApiError { ... }
   ```

5. **DO leverage the error chain**
   ```rust
   // Each level adds context
   low_level_op()
       .context(LowLevelSnafu)?
       .middle_layer()
       .context(MiddleLayerSnafu)?
       .high_level()
       .context(HighLevelSnafu)?;
   ```

6. **DO use `ensure!` for validation**
   ```rust
   ensure!(value > 0, InvalidValueSnafu { value });
   ```

### ❌ Don'ts

1. **DON'T use system backtraces in production**
   ```rust
   // Avoid this in production
   std::env::set_var("RUST_BACKTRACE", "1");
   ```

2. **DON'T ignore error context**
   ```rust
   // Bad: loses context
   operation().map_err(|_| MyError::Generic)?;

   // Good: preserves context
   operation().context(SpecificSnafu)?;
   ```

3. **DON'T create deeply nested error types**
   ```rust
   // 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 },
   }
   ```

4. **DON'T expose internal errors directly to users**
   ```rust
   // Bad: exposes implementation details
   return Err(internal_error);

   // Good: map to user-friendly error
   return Err(map_to_external_error(internal_error));
   ```

5. **DON'T forget to handle all error variants**
   ```rust
   // Use exhaustive matching
   match error {
       Error::Variant1 => handle_1(),
       Error::Variant2 => handle_2(),
       // Don't use _ => {} unless necessary
   }
   ```

6. **DON'T mix error handling strategies**
   ```rust
   // Pick one approach and stick to it
   // Either use SNAFU throughout or another error library
   // Don't mix multiple error handling crates
   ```

## How It Works

1. **Proc Macro Magic**: The `#[stack_trace_debug]` attribute automatically implements:
   - `VirtualStackTrace` trait for stack frame collection
   - Custom `Debug` implementation for formatted output

2. **Location Tracking**: Uses Rust's `#[track_caller]` to capture precise locations where errors are propagated

3. **Error Chain Walking**: Automatically traverses the `source()` chain to build complete error context

4. **Zero-Cost Until Needed**: Stack frames are only generated when the error is actually inspected

## API Reference

### Core Traits

#### `VirtualStackTrace`
```rust
pub trait VirtualStackTrace {
    fn virtual_stack(&self) -> Vec<StackFrame>;
}
```

#### `StackFrame`
```rust
pub struct StackFrame {
    pub location: &'static std::panic::Location<'static>,
    pub message: String,
}
```

### Attributes

#### `#[stack_trace_debug]`
Attribute macro that automatically implements virtual stack trace functionality for SNAFU error enums.

## Contributing

Contributions are welcome! Please feel free to submit issues and pull requests.

## License

This project is licensed under the Apache 2.0 License - see the LICENSE file for details.

## Acknowledgments

- Heavily inspired by [GreptimeDB's error handling approach]https://greptime.com/blogs/2024-05-07-error-rust
- Built on top of the excellent [SNAFU]https://github.com/shepmaster/snafu error library
- Thanks to the Rust community for amazing error handling discussions

## Related Resources

- [GreptimeDB Error Handling Blog Post]https://greptime.com/blogs/2024-05-07-error-rust
- [SNAFU Documentation]https://docs.rs/snafu
- [Rust Error Handling Book]https://doc.rust-lang.org/book/ch09-00-error-handling.html