Rust 36 🦀 Application Error Reporting (thiserror and anyhow)
Rust’s Result type is explicit about failure, but the standard library does not answer a harder question: what should the error type be? In a small function, a String or a custom enum works. In a real application, errors come from many sources — I/O, parsing, network, database, validation — and they need to propagate through layers of code without losing context. Writing a custom error enum by hand for every combination of failure modes is tedious and error-prone. The thiserror and anyhow crates exist to remove that tedium, and they do it in two different ways.
thiserror is for libraries. It generates the boilerplate implementations of Display, Error, and From for a hand-written error enum, so that the enum remains the public API of the crate. Callers can match on the variants, and the error type is part of the crate’s contract. The derive macro does not appear in the public API; switching from thiserror to a hand-written implementation is not a breaking change . anyhow is for applications. It provides a single, opaque error type that can hold any error implementing std::error::Error. The application does not need to name the error type in its signatures, and it can attach context to errors as they propagate with .context() or .with_context() .
The two crates are not competitors. They are designed to be used together. A library defines its errors with thiserror, and an application uses anyhow to propagate those errors and add application-level context. The conversion is automatic: a thiserror enum implements std::error::Error, so anyhow::Error can absorb it through the ? operator with no glue code . The result is a layered system where the library provides matchable errors and the application provides readable reports.
This chapter covers three areas. First, why structured and dynamic error reporting exist — the problem of error type design and the library-versus-application boundary. Second, how thiserror works — the derive macro, the #[error] attribute, #[from], #[source], and #[error(transparent)]. Third, how anyhow works — the anyhow::Error type, context methods, the anyhow! and bail! macros, and downcasting. The chapter ends with a complete example session, a quick reference, best practices, common pitfalls, real-world examples, and diagrams showing the error propagation layers.
Key point: thiserror is for libraries that need matchable error types as part of their public API. anyhow is for applications that need to propagate any error with context and produce readable reports. Use them together: libraries define thiserror enums, applications absorb them into anyhow::Error and add context with .context() .
Why thiserror and anyhow exist
The boilerplate problem. The std::error::Error trait requires a Display implementation, a Debug implementation, and optionally a source method. For an error enum with five variants, this means writing a Display match arm for each variant, a From implementation for each error type that converts into the enum, and a source method that returns the underlying error. None of this is difficult, but all of it is mechanical. thiserror generates it from attributes, so the developer writes the enum and the messages, and the macro writes the trait implementations .
The library-boundary problem. A library’s error type is part of its public API. When a caller uses the library, it needs to know what can fail. A String error loses that information — the caller cannot distinguish a file-not-found error from a parse error. A custom enum preserves it. But writing a custom enum by hand is tedious, and the tediousness encourages shortcuts like Box<dyn Error> or String, which discard the type information. thiserror makes the correct approach — a concrete, matchable enum — as easy as the incorrect one .
The application-ergonomics problem. An application does not have the same constraints as a library. The top-level code does not need to match on error variants; it needs to log the error, display a message to the user, and exit with a non-zero code. Naming every possible error type in every function signature is friction without benefit. anyhow::Error removes that friction. A function returns anyhow::Result<T>, uses ? freely, and the error type is inferred. The application can still recover specific errors with downcast_ref if needed, but the default path is propagation and reporting, not matching .
The context problem. A low-level error like “No such file or directory” is technically accurate but operationally useless without knowing which file the program was trying to read and why. anyhow solves this with .context() and .with_context(). Each call wraps the error in a layer that describes the operation that failed. When the error is printed, the layers form a chain: the outermost context is the high-level operation, and the innermost source is the original system error. This makes debugging production failures dramatically faster .
The conversion-direction problem. The integration between the two crates has a specific direction. A thiserror enum flows up into anyhow::Error automatically, because anyhow::Error implements From<E> for E: std::error::Error + Send + Sync + 'static. The reverse — extracting a typed error from an anyhow::Error — requires downcast_ref, and it is a signal that the recovery logic probably belonged in the library, not the application. Using anyhow in a library’s public API is considered a design mistake, because it forces every caller to depend on anyhow and discards matchability .
The trade-off. thiserror requires the developer to design the error enum: which variants exist, which errors convert into which variants, which context is part of the error and which is added later. This is work, but it is work that produces a clear API. anyhow requires almost no design: any error goes into anyhow::Error. This is convenient, but it means the error type carries no compile-time information. An application that needs to recover from a specific error must downcast, and downcasting is a runtime check that can fail silently if the error type is not what was expected. The trade-off is between the type safety of a designed enum and the ergonomics of an opaque error type.
a. thiserror for structured library errors
The thiserror crate provides a derive macro for std::error::Error. The macro generates Display, From, and Error implementations from attributes on the enum or struct .
The core attribute is #[error("...")], which defines the Display message. The message can interpolate fields from the variant:
use thiserror::Error;
#[derive(Error, Debug)]
pub enum ConfigError {
#[error("I/O error: {0}")]
Io(#[from] std::io::Error),
#[error("invalid port: {0}")]
InvalidPort(#[from] std::num::ParseIntError),
#[error("missing required field: {field}")]
MissingField { field: String },
#[error("value {value} out of range [{min}, {max}]")]
OutOfRange { value: f64, min: f64, max: f64 },
}
The #[from] attribute generates a From implementation for the variant. When a function returns Result<T, ConfigError> and calls a function that returns Result<T, std::io::Error>, the ? operator converts the io::Error into ConfigError::Io automatically. The variant must contain only the source error (and optionally a Backtrace field) .
The #[source] attribute marks a field as the underlying cause. The Error::source() method returns it. The #[from] attribute implies #[source], so specifying both is redundant. If a field is named source, thiserror recognizes it automatically .
The #[error(transparent)] attribute delegates both Display and source() to the wrapped error. This is useful when a variant exists only to wrap another error type without adding a message:
#[derive(Error, Debug)]
pub enum AppError {
#[error(transparent)]
Database(#[from] sqlx::Error),
#[error("not found: {resource} with id {id}")]
NotFound { resource: String, id: i64 },
}
The Database variant displays whatever sqlx::Error displays and returns it as the source. This preserves the error chain without inventing a new message .
thiserror supports enums, structs with named fields, tuple structs, and unit structs. The #[error] message supports format strings with field references, including arbitrary expressions and method calls on fields .
b. anyhow for application error propagation
The anyhow crate provides anyhow::Error, a trait object-based error type that can hold any error implementing std::error::Error. The type alias anyhow::Result<T> is Result<T, anyhow::Error> .
The primary use is propagation with ?:
use anyhow::Result;
fn load_config(path: &str) -> Result<Config> {
let content = std::fs::read_to_string(path)?;
let config: Config = serde_yaml::from_str(&content)?;
Ok(config)
}
Any error type that implements std::error::Error can be converted into anyhow::Error through ?. This includes std::io::Error, serde_yaml::Error, and any thiserror enum .
The .context() method attaches a human-readable message to the error. The .with_context() method takes a closure that produces the message lazily, which is useful when the message requires formatting that should only happen on the error path :
use anyhow::{Context, Result};
fn load_user(id: u64) -> Result<User> {
let path = format!("data/users/{id}.json");
let text = std::fs::read_to_string(&path)
.with_context(|| format!("failed to read user file '{path}'"))?;
let user: User = serde_json::from_str(&text)
.context("failed to deserialize user")?;
Ok(user)
}
When an anyhow::Error is printed, the context layers form a chain. The outermost layer is the most recent context, and the innermost is the original source error .
The anyhow! macro constructs an ad-hoc error from a string. The bail! macro is a shorthand for returning an error early. The ensure! macro checks a condition and returns an error if it is false :
use anyhow::{anyhow, bail, ensure, Result};
fn validate_config(config: &Config) -> Result<()> {
ensure!(!config.database_url.is_empty(), "database_url must not be empty");
if config.port == 0 {
bail!("port must be non-zero");
}
if config.port > 65535 {
return Err(anyhow!("port {} exceeds maximum", config.port));
}
Ok(())
}
Downcasting allows an application to recover a specific error type from an anyhow::Error. The downcast_ref::<T>() method returns Option<&T> if the error was originally of type T. This is used sparingly, typically when the application needs to check for a specific condition (such as a “not found” error that should produce a different response) .
c. The library-application boundary
The decision of which crate to use in which context follows a clear boundary. Libraries define error types with thiserror and expose them in their public API. Applications use anyhow for their own functions and propagate library errors through it .
// Library code (thiserror)
pub fn parse_port(input: &str) -> Result<u16, ConfigError> {
let port: u16 = input.parse().map_err(ConfigError::InvalidPort)?;
if port < 1024 {
return Err(ConfigError::ReservedPort(port));
}
Ok(port)
}
// Application code (anyhow)
fn initialize() -> anyhow::Result<u16> {
let port = parse_port("8080")
.context("failed to parse server port")?;
Ok(port)
}
The ConfigError from the library flows into anyhow::Error through ?. The application adds context with .context(). The library never depends on anyhow; the application never defines its own error enum unless it needs to expose one to its own callers.
The reverse direction — an application that needs to match on a library error — uses downcast_ref. If the application finds itself downcasting frequently, that is a signal that the error handling logic should be moved into the library or that the application should define its own error enum with thiserror.
Complete Example Session
// ============================================
// PART 1: THE LIBRARY ERROR TYPE (thiserror)
// ============================================
use thiserror::Error;
#[derive(Error, Debug)]
pub enum DataStoreError {
#[error("data store disconnected")]
Disconnect(#[from] std::io::Error),
#[error("the data for key `{0}` is not available")]
Redaction(String),
#[error("invalid header (expected {expected:?}, found {found:?})")]
InvalidHeader {
expected: String,
found: String,
},
#[error("unknown data store error")]
Unknown,
}
// ============================================
// PART 2: USING THE LIBRARY ERROR
// ============================================
pub fn read_config(path: &str) -> Result<String, DataStoreError> {
let content = std::fs::read_to_string(path)?;
// ^ io::Error -> DataStoreError::Disconnect
Ok(content)
}
// The `?` operator converts io::Error into
// DataStoreError::Disconnect via the #[from] impl.
// ============================================
// PART 3: THE APPLICATION ERROR HANDLING (anyhow)
// ============================================
use anyhow::{Context, Result};
fn load_user_data(user_id: &str) -> Result<String> {
let path = format!("users/{}.json", user_id);
let data = std::fs::read_to_string(&path)
.with_context(|| format!("failed to read user data from {}", path))?;
// ^ io::Error -> anyhow::Error with context
let id: i32 = user_id.parse()
.context("user ID must be a valid number")?;
// ^ ParseIntError -> anyhow::Error with context
if id < 0 {
anyhow::bail!("user ID must be non-negative");
}
Ok(data)
}
// ============================================
// PART 4: CONVERTING BETWEEN THE TWO
// ============================================
fn initialize_app() -> Result<()> {
// DataStoreError flows into anyhow::Error
let config = read_config("config.toml")
.context("failed to load configuration")?;
// ^ DataStoreError -> anyhow::Error with context
let data = load_user_data("42")?;
println!("config: {}, data: {}", config, data);
Ok(())
}
// The conversion is automatic:
// DataStoreError implements std::error::Error
// anyhow::Error implements From<DataStoreError>
// ============================================
// PART 5: ERROR CHAIN DISPLAY
// ============================================
fn main() {
match initialize_app() {
Ok(()) => println!("success"),
Err(e) => {
eprintln!("Error: {:#}", e);
// The {:#} format prints the full chain:
// Error: failed to load configuration
// Caused by:
// data store disconnected
// Caused by:
// No such file or directory (os error 2)
}
}
}
// ============================================
// PART 6: USING bail! AND ensure!
// ============================================
use anyhow::{bail, ensure};
fn process_request(request_id: &str) -> Result<()> {
let data = std::fs::read_to_string("request.json")
.with_context(|| format!("failed to read request {}", request_id))?;
ensure!(!data.is_empty(), "empty request data for {}", request_id);
if request_id.starts_with("invalid") {
bail!("invalid request ID: {}", request_id);
}
Ok(())
}
// ============================================
// PART 7: DOWNCASTING
// ============================================
fn handle_error() -> Result<()> {
let result: Result<()> = Err(DataStoreError::Redaction("key".into()).into());
match result {
Ok(()) => println!("success"),
Err(e) => {
if let Some(store_err) = e.downcast_ref::<DataStoreError>() {
match store_err {
DataStoreError::Redaction(key) => {
println!("redacted key: {}", key);
}
_ => println!("other data store error"),
}
} else {
println!("unknown error: {}", e);
}
}
}
}
// ============================================
// PART 8: TRANSPARENT ERROR WRAPPING
// ============================================
#[derive(Error, Debug)]
pub enum AppError {
#[error(transparent)]
Database(#[from] sqlx::Error),
#[error("not found: {resource} with id {id}")]
NotFound { resource: String, id: i64 },
}
// The Database variant delegates Display and
// source() to the wrapped sqlx::Error.
// ============================================
// PART 9: CONTEXT LAYERING
// ============================================
fn read_config_file() -> Result<String> {
std::fs::read_to_string("config.toml")
.context("failed to read config file")
}
fn parse_port(content: &str) -> Result<u16> {
content
.parse()
.context("failed to parse port number")
}
fn initialize() -> Result<u16> {
let content = read_config_file()
.context("initialization failed")?;
let port = parse_port(&content)
.context("configuration parsing failed")?;
if port < 1024 {
bail!("port must be >= 1024, got {}", port);
}
Ok(port)
}
// The error chain shows every layer:
// initialization failed
// -> failed to read config file
// -> No such file or directory (os error 2)
// ============================================
// PART 10: THE COMPLETE PATTERN
// ============================================
// Library: thiserror enum
#[derive(Error, Debug)]
pub enum LibError {
#[error("invalid input: {0}")]
InvalidInput(String),
#[error("I/O error: {0}")]
Io(#[from] std::io::Error),
}
// Library function
pub fn lib_function(value: &str) -> Result<i32, LibError> {
if value.is_empty() {
return Err(LibError::InvalidInput("empty value".into()));
}
value.parse().map_err(|_| LibError::InvalidInput("not a number".into()))
}
// Application function
fn app_function() -> anyhow::Result<i32> {
let result = lib_function("42")
.context("library function failed")?;
Ok(result)
}
// main uses anyhow::Result and prints the chain
fn main() -> anyhow::Result<()> {
let value = app_function()?;
println!("value: {}", value);
Ok(())
}
The ten parts show the library error enum, its use in a library function, application error handling with anyhow, automatic conversion between the two, error chain display, bail! and ensure!, downcasting, transparent wrapping, context layering, and the complete library-application pattern.
Quick Reference
When to Use Which
| Context | Crate | Error Type |
|---|---|---|
| Library public API | thiserror | Concrete enum; callers can match |
| Application top-level | anyhow | anyhow::Error; opaque, context-rich |
| Library internal | Either | thiserror for API, anyhow for internal |
Binary main | anyhow | anyhow::Result<()> |
thiserror Attributes
| Attribute | Effect |
|---|---|
#[error("...")] | Generates Display |
#[from] | Generates From conversion |
#[source] | Marks the underlying error |
#[error(transparent)] | Delegates Display and source() |
anyhow Methods
| Method/Macro | Effect |
|---|---|
.context(msg) | Attach context (eager) |
| `.with_context( | |
anyhow!(msg) | Construct ad-hoc error |
bail!(msg) | Return error early |
ensure!(cond, msg) | Return error if condition false |
.downcast_ref::<T>() | Recover specific error type |
Error Chain Display
| Format | Output |
|---|---|
{} | Top-level message only |
{:#} | Full chain with “Caused by” |
{:?} | Debug format with chain |
Best Practices
✅ Do This:
// Use thiserror in libraries
#[derive(Error, Debug)]
pub enum ConfigError { #[error("...")] Io(#[from] io::Error) } // ✅
// Use anyhow in applications
fn load() -> anyhow::Result<Config> { ... } // ✅
// Add context at abstraction boundaries
.with_context(|| format!("failed to read {}", path))? // ✅
// Use #[from] for automatic conversions
#[error("I/O error: {0}")] Io(#[from] std::io::Error) // ✅
// Preserve the error chain
#[error(transparent)] Database(#[from] sqlx::Error) // ✅
// Use bail! for early returns
bail!("port must be non-zero"); // ✅
❌ Don’t Do This:
// Don't use anyhow in library public API
pub fn lib_fn() -> anyhow::Result<()> { ... } // ❌
// Don't lose the source error
#[error("I/O error")] Io(std::io::Error) // no source() // ❌
// Don't use String errors in libraries
fn parse() -> Result<(), String> { ... } // ❌
// Don't forget context on fallible operations
std::fs::read_to_string(path)?; // no context // ❌
// Don't downcast at the application layer
if e.downcast_ref::<ConfigError>().is_some() { ... } // code smell // ❌
// Don't derive Debug without it
#[derive(Error)] pub enum E { ... } // missing Debug // ❌
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
anyhow in library API | Convenience | Use thiserror enum |
| Error chain lost | No #[source] or #[from] | Add #[from] or #[source] |
| Context missing | Forgot .context() | Add context at boundaries |
| Downcast failures | Wrong error type or lost type | Check error origin |
#[error(transparent)] no message | Delegates to wrapped error | Add message if needed |
Variant with extra fields and #[from] | #[from] requires only source | Split into separate variants |
Debug not derived | Forgot derive | Always #[derive(Debug)] |
Real-World Examples
1. Library Error Enum
#[derive(Error, Debug)]
pub enum ConfigError { #[error("...")] Io(#[from] io::Error) }
2. Application Result
fn run() -> anyhow::Result<()> { ... }
3. Context
std::fs::read_to_string(path).context("failed to read config")?
4. with_context
.with_context(|| format!("failed to read {}", path))?
5. bail!
bail!("port must be non-zero");
6. ensure!
ensure!(!config.url.is_empty(), "URL required");
7. Transparent
#[error(transparent)] Database(#[from] sqlx::Error)
8. Downcast
e.downcast_ref::<ConfigError>()
9. Error Chain
eprintln!("{:#}", e);
10. Conversion
// thiserror enum -> anyhow::Error via ?
Visual
The Library-Application Boundary
┌──────────────────────────────────────────────────────────────┐
│ APPLICATION (binary crate) │
│ │
│ main() -> anyhow::Result<()> │
│ │ │
│ │ uses anyhow::Error │
│ │ .context("loading config") │
│ │ │
│ │ does NOT match on variants │
│ │ cares about readable report + exit code │
│ │ │
│ ├──► library_function() -> Result<T, ConfigError> │
│ │ │ │
│ │ ▼ │
│ │ LIBRARY (shared crate) │
│ │ ┌──────────────────────────────────────────────┐ │
│ │ │ pub enum ConfigError (thiserror) │ │
│ │ │ #[from] wires source errors │ │
│ │ │ │ │
│ │ │ errors ARE the public API │ │
│ │ │ callers CAN match on variants │ │
│ │ │ never depends on anyhow │ │
│ │ └──────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ConfigError flows UP into anyhow::Error through ? │
│ │
└──────────────────────────────────────────────────────────────┘
Error Chain Layers
┌──────────────────────────────────────────────────────────────┐
│ ERROR CHAIN │
│ │
│ Display output: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Error: failed to load configuration │ │
│ │ Caused by: │ │
│ │ data store disconnected │ │
│ │ Caused by: │ │
│ │ No such file or directory (os error 2) │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ Layer structure: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ .context("failed to load configuration") │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ DataStoreError::Disconnect │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ std::io::Error │ │
│ │ "No such file or directory" │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ Each layer adds context. The innermost is the source. │
│ │
└──────────────────────────────────────────────────────────────┘
thiserror Code Generation
┌──────────────────────────────────────────────────────────────┐
│ THISERROR CODE GENERATION │
│ │
│ Source: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ #[derive(Error, Debug)] │ │
│ │ pub enum ConfigError { │ │
│ │ #[error("I/O error: {0}")] │ │
│ │ Io(#[from] std::io::Error), │ │
│ │ } │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ Generated: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ impl Display for ConfigError { │ │
│ │ fn fmt(...) { │ │
│ │ match self { │ │
│ │ ConfigError::Io(e) => write!("I/O error: {}", e)│ │
│ │ } │ │
│ │ } │ │
│ │ } │ │
│ │ │ │
│ │ impl From<std::io::Error> for ConfigError { │ │
│ │ fn from(e: io::Error) -> Self { │ │
│ │ ConfigError::Io(e) │ │
│ │ } │ │
│ │ } │ │
│ │ │ │
│ │ impl std::error::Error for ConfigError { │ │
│ │ fn source(&self) -> Option<&dyn Error> { │ │
│ │ match self { │ │
│ │ ConfigError::Io(e) => Some(e) │ │
│ │ } │ │
│ │ } │ │
│ │ } │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────┘
anyhow Conversion
┌──────────────────────────────────────────────────────────────┐
│ ANYHOW CONVERSION │
│ │
│ Library function: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ fn read_config() -> Result<String, ConfigError> │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ Application function: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ fn initialize() -> anyhow::Result<()> { │ │
│ │ let config = read_config() │ │
│ │ .context("failed to load config")?; │ │
│ │ // ^ ConfigError -> anyhow::Error │ │
│ │ Ok(()) │ │
│ │ } │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ The ? operator calls: │
│ From<ConfigError> for anyhow::Error │
│ │
│ This works because: │
│ ConfigError: std::error::Error + Send + Sync + 'static │
│ │
└──────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
thiserror | Derive macro for std::error::Error |
| Use in | Libraries and shared crates |
| Error type | Concrete enum; callers can match |
#[error("...")] | Generates Display |
#[from] | Generates From conversion |
#[source] | Marks underlying error |
#[error(transparent)] | Delegates Display and source() |
anyhow | Opaque error type for applications |
| Use in | Binaries and top-level application code |
.context() | Attach context eagerly |
.with_context() | Attach context lazily |
anyhow! | Construct ad-hoc error |
bail! | Return error early |
ensure! | Return error if condition false |
| Conversion | thiserror enum → anyhow::Error via ? |
Key takeaways:
thiserroris for libraries. It generates the boilerplate for a hand-written error enum, keeping the enum as the public API. Callers can match on the variants, and the error type is part of the crate’s contract .anyhowis for applications. It provides a single opaque error type that can hold anystd::error::Error. The application does not name the error type in its signatures, and it can attach context with.context()and.with_context().- The conversion is automatic in one direction. A
thiserrorenum implementsstd::error::Error, soanyhow::Errorabsorbs it through?. The reverse requiresdowncast_ref, and frequent downcasting is a signal that the handling belongs in the library . #[from]generates theFromimplementation. A variant with#[from]is automatically converted from the source error type. The variant must contain only the source error (and optionally aBacktrace) .#[error(transparent)]delegatesDisplayandsource(). This is useful for wrapper variants that add no message of their own and only serve to include another error type in the enum .- Context layers build the error chain. Each
.context()call wraps the error in a layer that describes the operation that failed. The{:#}format prints the full chain with “Caused by” sections . bail!andensure!simplify early returns.bail!constructs an error and returns it.ensure!checks a condition and returns an error if it is false .- Always derive
Debug. TheDebugimplementation is required for logging and debugging, andthiserror‘sErrorderive requires it as well.
Remember: Error handling in Rust has two layers. The library layer needs matchable, documented error types, and thiserror makes those types cheap to define. The application layer needs ergonomic propagation and readable reports, and anyhow provides both. The two layers connect through std::error::Error: a thiserror enum implements the trait, and anyhow::Error absorbs anything that implements it. The result is a system where libraries expose exactly what can fail, and applications add the context that turns a low-level error into an operational diagnosis. Use thiserror when the error type is part of your API; use anyhow when the error is something to report, not something to match.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!