handle-this
Ergonomic error handling for Rust with automatic stack traces.
This Is Not Exception Handling
The syntax looks like try/catch, but the semantics are pure Rust. Understanding this upfront will save confusion:
In exception-based languages:
try { risky() }
catch { handle() } // catches thrown exception, unwinds stack
throw new Error() // throws exception, unwinds stack
In handle-this:
try
catch // pattern matches Err, returns Ok(handle())
throw // transforms Err, continues to NEXT handler
The critical differences:
| Exceptions | handle-this | |
|---|---|---|
| Mechanism | Stack unwinding | Result<T, E> |
throw |
Exits immediately | Transforms error, continues chain |
catch |
Catches thrown exception | Matches Err, returns Ok |
| Propagation | Implicit | Explicit with ? |
| Performance | Zero-cost try, expensive throw | Zero-cost success, cheap error path |
Why this matters: If you expect throw to exit like in Java/Python/JavaScript, you'll be confused. In handle-this, throw is a transformation step, not an exit point. The error continues through subsequent handlers until a catch stops it.
handle!
// Returns: Ok("recovered")
Why handle-this?
Rust's ? operator is great for simple propagation, but real-world error handling often requires more:
// Without handle-this: verbose, scattered logic
// With handle-this: clear, declarative
What you get:
- Automatic stack traces — Every error captures its origin and propagation path
- Zero-cost success path — No overhead when operations succeed
- Type-safe matching — Pattern match on error types with guards
- Composable handlers — Chain multiple handlers in declaration order
- Iteration patterns — First-success, collect-all, retry with backoff
- Tested exhaustively — 210k+ generated tests covering all syntax permutations
Installation
[]
= "0.2"
MSRV: 1.70.0
VS Code Extension
Get syntax highlighting for handle! macro keywords (try, catch, throw, inspect, finally, when, scope, etc.) with the included VS Code extension.
Why install it? Without the extension, VS Code's Rust syntax highlighter doesn't recognize handle-this keywords, making the code harder to read. The extension adds proper highlighting and a color picker to customize the appearance.
Install from VSIX:
# From the repository root
Or build from source:
After installation, use the command palette (Ctrl+Shift+P) → "handle-this: Configure Colors" to customize highlighting.
Quick Start
use ;
// Wrap errors with automatic location tracking
// Recover from errors (returns String, not Result)
// Multiple typed handlers
Core Concepts
The Handler Chain
Handlers form a pipeline that processes errors. Understanding terminal vs non-terminal is key:
| Handler | Terminal? | What it does |
|---|---|---|
catch |
Yes | Matches error → returns Ok(value) → chain stops |
try catch |
Yes | Matches error → body returns Result → chain stops |
throw |
No | Matches error → transforms it → continues to next handler |
inspect |
No | Matches error → runs side effect → continues to next handler |
handle!
The error flows through ALL matching non-terminal handlers until a terminal handler catches it.
Execution Order
Handlers execute top-to-bottom in declaration order:
handle!
Critical Rules
- Untyped catch must be last — catches everything, makes subsequent handlers unreachable
- throw changes the type — typed catches after throw may not match
- inspect never stops — always propagates after running
// COMPILE ERROR: catch e {} catches everything
handle!
// CORRECT: typed catches before untyped
handle!
Patterns Reference
Basic Patterns
// Just wrap with location
try
// Catch with binding
try catch e
// Catch without binding
try catch
// Explicit discard
try catch _
// Infallible (returns T, not Result<T>)
try else
// Transform error
try throw e
// Side effect then propagate
try inspect e
// Chain operations (pass success values through)
try , then |x| , then |y|
Typed Handlers
// Match specific type
catch Error
catch Error // no binding
// Type with fallback
catch Error
else
// Typed throw
throw ParseError
// Typed inspect
inspect NetworkError
Guards
// Catch with condition
catch e when is_retryable
// Typed with guard
catch Error when e.kind == NotFound
// Guard on throw
throw Error when e.kind == TimedOut
Match Clause
catch Error match e.kind
Error Chain Search
For wrapped errors, search the cause chain:
// First matching error in chain
catch any Error
// All matching errors as Vec
catch all ValidationError |errors|
// Chain search with guard
catch any Error when e.kind == NotFound
Iteration Patterns
// First success (try servers until one works)
try for server in servers
catch
// Collect all successes
try all item in items
catch // returns Vec of successes
// Retry with condition
let mut attempts = 0;
try while attempts < 3
Context and Scope
// Add context message
try with "processing request"
// Context with structured data
try with "db query",
// Structured data only
try with
// Hierarchical scope
scope "http handler",
try
Cleanup
try
finally
catch e
Preconditions
handle!
Nested Patterns
Try blocks can nest freely—inner handlers catch their own errors:
handle!
Then Chains
Chain operations together, passing success values through a pipeline:
handle!
Each then receives the Ok value from the previous step. Type annotations are supported:
handle!
Context can be added to any step using all with variants:
handle!
All handlers (catch, throw, inspect, finally) work with chains.
Async Support
All patterns work with async:
handle!
Control Flow
Loop handlers support break/continue to outer loops:
'outer: for batch in batches
Stack Traces
Every error captures its propagation path automatically:
// Error displays:
// root cause
//
// Trace (most recent last):
// src/lib.rs:2:5
// → in inner
// src/lib.rs:6:5
// → in outer
Structured data appears in traces:
with "processing order",
// Trace shows:
// src/orders.rs:42:5
// → processing order
// order_id: 12345
// customer: "acme"
Performance
Success path has zero overhead. Error path cost depends on what you access.
| Scenario | handle-this | Rust | Overhead |
|---|---|---|---|
| Success path | 1.5ns | 1.5ns | 1.0x |
| Typed catch + fallback | 62ns | 63ns | 1.0x |
| Multi-handler chain | 60ns | 59ns | 1.0x |
| Nested error handling | 61ns | 61ns | 1.0x |
| Realistic workload | 63ns | 63ns | 1.0x |
| Error message access | 106ns | 88ns | 1.2x |
| Full trace format | 384ns | 88ns | 4.4x |
| Typed catch miss | 86ns | 63ns | 1.4x |
Key tradeoffs:
- Success path: Zero overhead
- Error caught: Zero overhead when using typed catches with fallback
- Message access: 1.2x overhead for
e.message()(lazy computation) - Full trace: 4.4x overhead for
e.to_string()(formats all locations) - Type miss: 1.4x overhead when error propagates (wrapping cost)
Use e.message() instead of e.to_string() when you only need the error text.
Feature Flags
| Feature | Description |
|---|---|
std (default) |
Standard library support |
serde |
Serialize/deserialize errors |
anyhow |
Convert from anyhow::Error |
eyre |
Convert from eyre::Report |
Comparison
| Feature | handle-this | anyhow | thiserror |
|---|---|---|---|
| Automatic stack traces | Yes | RUST_BACKTRACE | No |
| Typed error matching | Yes | Downcast | Define types |
| Guard conditions | Yes | No | No |
| try/catch syntax | Yes | No | No |
| Error transformation | throw |
.context() |
Manual |
| Iteration patterns | Yes | No | No |
| no_std support | Yes | No | Yes |
Testing
The macro is validated by 210k+ generated tests covering:
- All handler combinations (single, two, three handlers)
- All binding variants (named, underscore, typed, untyped)
- All guard conditions
- All iteration patterns
- Nested try patterns at multiple depths
- Async variants
- Control flow (break/continue) in handlers
# Run all tests
# Run specific matrix
License
Licensed under either of Apache License, Version 2.0 or MIT license at your option.