31 breaking releases
0.33.0 | Sep 6, 2025 |
---|---|
0.31.0 | Aug 12, 2025 |
0.26.0 | Jul 31, 2025 |
0.19.0 | Oct 30, 2024 |
0.1.4 | Jun 12, 2022 |
#112 in Algorithms
2,097 downloads per month
Used in 61 crates
(20 directly)
56KB
163 lines
Module :: error_tools
A unified error handling facade that provides a consistent interface for both typed and untyped error handling in Rust. error_tools
acts as a standardized wrapper around the popular thiserror
and anyhow
crates, enabling you to write error-handling code once and use it consistently across different contexts.
Why error_tools?
When building Rust applications and libraries, you often face these error handling challenges:
- Library vs Application Choice: Libraries typically use
thiserror
for typed errors, while applications preferanyhow
for flexibility - Inconsistent Error Patterns: Different crates in your dependency tree use different error handling approaches
- Dependency Fragmentation: Having both
anyhow
andthiserror
as direct dependencies across multiple crates - Context Switching: Different syntax and patterns for similar error handling tasks
- Integration Friction: Converting between different error types when bridging library and application code
error_tools solves these problems by providing:
- 🎯 Unified Interface: Single import pattern for both typed and untyped errors
- 📦 Dependency Facade: Centralized re-export of
anyhow
andthiserror
functionality - 🔧 Enhanced Utilities: Additional error handling utilities like
ErrWith
trait - 🏗️ Consistent Patterns: Standardized error handling across the entire wTools ecosystem
- 🚀 Easy Migration: Drop-in replacement for existing
anyhow
/thiserror
usage - 🛡️ no_std Support: Works in
no_std
environments when needed
Quick Start
Installation
cargo add error_tools
Basic Usage
Choose your approach based on your needs:
// For applications - flexible, untyped errors (anyhow-style)
use error_tools::untyped::*;
// For libraries - structured, typed errors (thiserror-style)
use error_tools::typed::*;
use error_tools::dependency::thiserror;
// For convenience - includes both
use error_tools::prelude::*;
Core Concepts
1. Untyped Errors (Application-Focused)
Perfect for applications where you need flexible error handling without defining custom error types for every possible failure. This is a direct facade over anyhow
.
Key Features:
- Dynamic error handling with context
- Easy error chaining and reporting
- Rich context information
- Perfect for rapid prototyping and applications
use error_tools::untyped::{ Result, format_err };
fn get_message() -> Result< &'static str >
{
Ok( "Hello, world!" )
// Err( format_err!( "An unexpected error!" ) )
}
fn main()
{
match get_message()
{
Ok( msg ) => println!( "Success: {}", msg ),
Err( e ) => println!( "Error: {:?}", e ),
}
}
Run this example:
cargo run --example error_tools_trivial
2. Working with Context
Adding context to errors helps with debugging and user experience:
use error_tools::untyped::{ Result, Context, format_err };
fn read_and_process_file( path : &str ) -> Result< String >
{
// Simulate file reading for demonstration
let content = if path == "test.txt" { "hello world" } else { "" };
if content.is_empty()
{
return Err( format_err!( "File is empty or not found: {}", path ) );
}
Ok( content.to_uppercase() )
}
fn main()
{
match read_and_process_file( "test.txt" )
{
Ok( content ) => println!( "Processed: {}", content ),
Err( e ) => println!( "Error: {}", e ),
}
}
See the full runnable example in
examples/replace_anyhow.rs
.
3. Typed Errors (Library-Focused)
Ideal for libraries where you want to provide a clear, structured contract for possible errors. This is a facade over thiserror
.
Key Features:
- Structured error types with derive macros
- Clear error hierarchies
- Compile-time error checking
- Better API boundaries for library consumers
use error_tools::typed::Error;
use error_tools::dependency::thiserror;
#[ derive( Debug, Error ) ]
pub enum DataError
{
#[ error( "I/O error for file: {file}" ) ]
Io { file : String },
#[ error( "Parsing error: {0}" ) ]
Parse( String ),
}
fn process_data( file_name : &str, content : &str ) -> Result< i32, DataError >
{
if content.is_empty()
{
return Err( DataError::Io { file : file_name.to_string() } );
}
content.trim().parse::< i32 >()
.map_err( | _ | DataError::Parse( "Could not parse content as integer".into() ) )
}
fn main()
{
match process_data( "data.txt", "123" )
{
Ok( num ) => println!( "Parsed number: {}", num ),
Err( e ) => println!( "Error: {}", e ),
}
// Example with error
match process_data( "invalid.txt", "abc" )
{
Ok( _ ) => (),
Err( e ) => println!( "Expected error: {}", e ),
}
}
See the full runnable example in
examples/replace_thiserror.rs
.
4. Enhanced Error Context with ErrWith
The ErrWith
trait provides additional utilities for adding context to errors:
use error_tools::{ ErrWith };
fn process_user_data( user_id : u32, data : &str ) -> Result< String, ( String, Box< dyn std::error::Error > ) >
{
// Add context using closures for lazy evaluation
let parsed_data = data.parse::< i32 >()
.err_with( || format!( "Failed to parse data for user {}", user_id ) )?;
// Add context using references for simple messages
let processed = perform_calculation( parsed_data )
.err_with_report( &format!( "Calculation failed for user {}", user_id ) )?;
Ok( format!( "Processed: {}", processed ) )
}
fn perform_calculation( input : i32 ) -> std::result::Result< i32, &'static str >
{
if input < 0
{
Err( "Negative numbers not supported" )
}
else
{
Ok( input * 2 )
}
}
fn main()
{
match process_user_data( 123, "42" )
{
Ok( result ) => println!( "Success: {}", result ),
Err( ( report, err ) ) => println!( "Error: {} - {:?}", report, err ),
}
}
See the full runnable example in
examples/err_with_example.rs
.
5. Debug Assertions
Additional debugging utilities for development:
use error_tools::{ debug_assert_id, debug_assert_ni };
fn validate_data( expected : &str, actual : &str )
{
// Only active in debug builds
debug_assert_id!( expected, actual, "Data validation failed" );
// Negative assertion
debug_assert_ni!( expected, "", "Expected data should not be empty" );
}
fn main()
{
validate_data( "test", "test" );
println!( "Debug assertions passed!" );
}
Examples
Basic Error Handling
use error_tools::untyped::Result;
fn might_fail( should_fail : bool ) -> Result< String >
{
if should_fail
{
Err( error_tools::untyped::format_err!( "Something went wrong" ) )
}
else
{
Ok( "Success!".to_string() )
}
}
fn main()
{
match might_fail( false )
{
Ok( msg ) => println!( "Result: {}", msg ),
Err( e ) => println!( "Error: {}", e ),
}
}
Using Both Typed and Untyped Errors
use error_tools::prelude::*;
use error_tools::dependency::thiserror;
// Typed error for library API
#[ derive( Debug, Error ) ]
pub enum ConfigError
{
#[ error( "Configuration file not found" ) ]
NotFound,
#[ error( "Invalid format: {0}" ) ]
InvalidFormat( String ),
}
// Function returning typed error
fn load_config_typed() -> Result< String, ConfigError >
{
Err( ConfigError::NotFound )
}
// Function returning untyped error
fn load_config_untyped() -> error_tools::untyped::Result< String >
{
Err( error_tools::untyped::format_err!( "Configuration loading failed" ) )
}
fn main()
{
// Handle typed error
if let Err( e ) = load_config_typed()
{
println!( "Typed error: {}", e );
}
// Handle untyped error
if let Err( e ) = load_config_untyped()
{
println!( "Untyped error: {}", e );
}
}
Feature Flags
error_tools
supports granular feature control:
[dependencies]
error_tools = { version = "0.26", features = [ "error_typed" ] } # Only typed errors
# or
error_tools = { version = "0.26", features = [ "error_untyped" ] } # Only untyped errors
# or
error_tools = { version = "0.26" } # Both (default)
Available Features:
default
- Enables botherror_typed
anderror_untyped
error_typed
- Enablesthiserror
integration for structured errorserror_untyped
- Enablesanyhow
integration for flexible errorsno_std
- Enablesno_std
supportuse_alloc
- Enables allocation support inno_std
environments
Migration Guide
From anyhow
Replace your anyhow
imports with error_tools::untyped
:
// Before
// use anyhow::{ Result, Context, bail, format_err };
// After
use error_tools::untyped::{ Result, Context, bail, format_err };
fn main() {
println!("Migration complete - same API, different import!");
}
Everything else stays the same!
From thiserror
Add the explicit thiserror
import and use error_tools::typed
:
// Before
// use thiserror::Error;
// After
use error_tools::typed::Error;
use error_tools::dependency::thiserror; // Required for derive macros
fn main() {
println!("Migration complete - same derive macros, unified import!");
}
The derive macros work identically.
Complete Examples
Explore these runnable examples in the repository:
# Basic usage patterns
cargo run --example error_tools_trivial
# Migration from anyhow
cargo run --example replace_anyhow
# Migration from thiserror
cargo run --example replace_thiserror
# Using the ErrWith trait
cargo run --example err_with_example
Best Practices
1. Choose the Right Error Style
- Applications: Use
untyped
errors for flexibility and rapid development - Libraries: Use
typed
errors for clear API contracts and better user experience - Mixed Projects: Use both as appropriate - they interoperate well
2. Error Context
Always provide meaningful context:
use error_tools::untyped::{ Result, Context, format_err };
fn process_user_data( user_id : u32 ) -> Result< String >
{
// Good - specific context
let _result = simulate_operation()
.context( format!( "Failed to process user {} data", user_id ) )?;
// Less helpful - generic context
let _other = simulate_operation()
.context( "An error occurred" )?;
Ok( "Success".to_string() )
}
fn simulate_operation() -> Result< String >
{
Ok( "data".to_string() )
}
fn main()
{
match process_user_data( 123 )
{
Ok( result ) => println!( "Result: {}", result ),
Err( e ) => println!( "Error: {}", e ),
}
}
3. Error Hierarchies
For libraries, design clear error hierarchies:
use error_tools::typed::Error;
use error_tools::dependency::thiserror;
#[ derive( Debug, Error ) ]
pub enum LibraryError
{
#[ error( "Configuration error: {0}" ) ]
Config( #[from] ConfigError ),
#[ error( "Network error: {0}" ) ]
Network( #[from] NetworkError ),
#[ error( "Database error: {0}" ) ]
Database( #[from] DatabaseError ),
}
// Define the individual error types
#[ derive( Debug, Error ) ]
pub enum ConfigError
{
#[ error( "Config not found" ) ]
NotFound,
}
#[ derive( Debug, Error ) ]
pub enum NetworkError
{
#[ error( "Connection failed" ) ]
ConnectionFailed,
}
#[ derive( Debug, Error ) ]
pub enum DatabaseError
{
#[ error( "Query failed" ) ]
QueryFailed,
}
fn main()
{
let config_err = LibraryError::Config( ConfigError::NotFound );
println!( "Error hierarchy example: {}", config_err );
}
4. Dependency Access
When you need direct access to the underlying crates:
// Access the underlying crates if needed
// use error_tools::dependency::{ anyhow, thiserror };
// Or via the specific modules
use error_tools::untyped; // Re-exports anyhow
use error_tools::typed; // Re-exports thiserror
fn main()
{
println!("Direct access to underlying crates available via dependency module");
}
Integration with wTools Ecosystem
error_tools
is designed to work seamlessly with other wTools crates:
- Consistent Error Handling: All wTools crates use
error_tools
for unified error patterns - Cross-Crate Compatibility: Errors from different wTools crates integrate naturally
- Standardized Debugging: Common debugging utilities across the ecosystem
To add to your project
cargo add error_tools
Try out from the repository
git clone https://github.com/Wandalen/wTools
cd wTools
cargo run --example error_tools_trivial
# Or try the specific examples
cargo run --example replace_anyhow
cargo run --example replace_thiserror
cargo run --example err_with_example
Dependencies
~130KB