Documentation
Everything you need to build structured, type-safe errors in Rust.
# Introduction
Waddling Errors is a structured diagnostic system for Rust. It solves the trade-off between human readability and machine efficiency by using a Dual Representation model.
The 5-Part Structure
Every error is composed of 5 parts that work together to provide complete context:
- 1Severity
One character (
E,W,I...) indicating urgency. - 2Component
High-level system module (e.g.,
Auth,Database). - 3Primary
Logical grouping within a component (e.g.,
Token,Connection). - 4Sequence
Specific numeric event identifier (e.g.,
001). - 5Compact ID (The Hash)
A 5-character, base62 hash generated deterministically from parts 1-4. Use this for APIs, databases, and customer support.
Why this matters?
For Humans & Machines (Structured)
Log files tell a story: E.Database.Conn.TIMEOUT. It's readable for humans AND allows machines to perform powerful tree-based searches (e.g., "Find all E.Auth.* errors").
For Efficiency (Compact)
APIs return Xy8Qz. It's tiny (5 chars), URL-safe, and never changes, even if you rename "Auth" to "Identity" internally.
# Installation
Getting started with waddling-errors is simple. It is designed to be modular, so you only pay for what you use. The library is no_std compatible by default and fully supports WASM environments.
Standard Installation
For most users (including those in no_std + alloc environments), simply add the latest version:
[dependencies]
waddling-errors = "0.7"Feature Flags
Control your footprint with granular feature flags.
[dependencies]
# Default (no_std + alloc compatible, includes macros)
waddling-errors = "0.7"
# With documentation generation (requires std)
waddling-errors = { version = "0.7", features = ["doc-gen"] }
# Pure no_std (no allocator)
waddling-errors = { version = "0.7", default-features = false }no_std Support & Constraints
The library is no_std by default and works in embedded systems, WASM, and constrained environments. Understanding what works in each environment helps you choose the right features.
| Environment | Config | Runtime Capabilities |
|---|---|---|
| Pure no_std | default-features = false | β
Error code constants β Severity checking β Compile-time hashes β String formatting β Collections (Vec, HashMap) |
| no_std + alloc | Default | β
Everything from pure no_std β String formatting β Procedural macros β Metadata collections β File I/O |
| With std | features = ["std", "doc-gen"] | β
Everything from no_std + alloc β File I/O β HTML/JSON/Catalog generation β Auto-registration |
π Doc Generation Workflow
Your library code can be no_std. Doc generation runs separately in a std environment (build script, CLI tool, or separate binary). The macros embed metadata at compile timeβa std-enabled tool reads it and writes HTML/JSON files. Your production binary stays clean.
Feature Dependencies
macros- Requires alloc (procedural macros)serde- Requires alloc (serialization)metadata- Requires alloc (collections)doc-gen- Requires std (file I/O for writing docs)auto-register- Requires std (initialization hooks)
Note: Hashing is always available at compile time via waddling-errors-hashβno feature flag needed. All environments get hashes; they're embedded as static strings.
Common Scenarios
// Embedded (ARM Cortex-M, no heap)
[dependencies]
waddling-errors = { version = "0.7", default-features = false }
// WASM (with allocator)
[dependencies]
waddling-errors = "0.7" # Default works!
// Server/Desktop (full std with doc generation)
[dependencies]
waddling-errors = { version = "0.7", features = ["std", "doc-gen", "auto-register"] }π‘ Design Philosophy
The library assumes no_std by default. The std feature is opt-in, ensuring embedded and WASM users don't pay for capabilities they can't use. Procedural macros run at compile time and don't increase binary size.
Without Macros (Manual Approach)
Prefer full control? You can define errors manually using traits instead of macros. See the crate documentation for the ComponentId and PrimaryId trait-based approach.
π More Examples
Explore complete working examples in the GitLab repository, including WASM, embedded (no_std), custom renderers, and role-based filtering.
# Quick Start
Get up and running in under 5 minutes.
1. Install
cargo add waddling-errorsOr add to Cargo.toml manually:
[dependencies]
waddling-errors = "0.7"
# For documentation generation (requires std)
waddling-errors = { version = "0.7", features = ["doc-gen"] }2. Define & Use
Here is a complete Zero to Hero example in a single file:
use waddling_errors_macros::{component, primary, sequence, diag, setup};
// 1. Setup macro paths (at crate root, before diag!)
setup! {
components = crate,
primaries = crate,
sequences = crate,
}
// 2. Define Architecture (Components & Primaries)
component! { Auth { docs: "Authentication" } }
primary! { Token { docs: "Token errors" } }
// 3. Define Events (Sequences)
sequence! {
MISSING(1) { description: "Token missing" },
EXPIRED(17) { description: "Token expired" }
}
// 4. Create Diagnostics
diag! {
E.Auth.Token.MISSING: {
message: "Authentication token was not provided",
hints: ["Check the Authorization header"],
},
E.Auth.Token.EXPIRED: {
message: "Token expired at {{time}}",
fields: [time],
}
}
fn main() {
// Use them!
println!("Code: {}", E_AUTH_TOKEN_MISSING.runtime.code); // "E.Auth.Token.001"
println!("Hash: {}", E_AUTH_TOKEN_MISSING.hash); // "Xy8Qz" (Compact ID)
}
# Defining Your Domain
Before creating errors, you define your system's skeleton. This maps your physical or logical architecture to Components and Primaries.
Components
Top-level system modules. Think "Service", "Library", or "Layer".
component! {
Auth { docs: "Authentication layer" },
Database { docs: "Persistence layer" }
}Primaries
Logical groupings within a component. Think "Noun" or "Feature".
primary! {
Token { docs: "Token errors" },
Connection { docs: "Connection errors" },
Query { docs: "Query errors" }
}Sequences
Global event identifiers. Sequences are reusable across different components/primaries.
sequence! {
// Standard WDP conventions
NOT_FOUND(21) { description: "Resource not found" },
ALREADY_EXISTS(22) { description: "Resource already exists" },
// Auth specific
EXPIRED(17) { description: "Token expired" },
INVALID(3) { description: "Invalid signature" }
}# Creating Diagnostics
The diag! macro binds your architecture (Component/Primary) to an event (Sequence) to create a concrete error.
Basic Usage
diag! {
// Sev.Comp.Prim.Seq
// β β β β
E.Auth.Token.MISSING: {
message: "No token provided",
}
}Dynamic Fields & PII
Use double curly braces {{field}} in messages to interpolate data. For sensitive data (PII), use the {{pii/field}} prefix to ensure it is redacted in secure contexts.
diag! {
E.Database.Connection.TIMEOUT: {
message: "Connection timed out after {{duration}}ms. User: {{pii/username}}",
fields: [duration],
pii: [username],
}
}| Field | Required | Type | Description |
|---|---|---|---|
| message | Yes | string | Template with {{field}} placeholders |
| fields | No | array | Non-PII field names |
| pii | No | array | PII field names (use {{pii/field}}) |
| severity | No | ident | Override default severity |
Metadata & Hints
Add helpful context for developers (docs) or users (hints).
diag! {
E.Auth.Token.EXPIRED: {
message: "Token is no longer valid",
// "Help" text for the final user
hints: ["Please log in again to refresh your token"],
// Documentation for the developer
docs: "Occurs when the JWT exp claim is in the past.",
}
}Code Snippets (Before/After Examples)
Show wrong vs. correct code examples to help developers fix errors quickly. At least one of wrong or correct is required.
diag! {
E.Auth.Token.MISSING: {
message: "Authorization header is missing",
code_snippet: {
language: "rust",
wrong: "let user = authenticate(); // β No token check",
correct: "let user = authenticate(request.token()?); // β
Token validated",
}
}
}Role-Based Visibility
Control what different audiences see using role markers ('Pub, 'Dev, 'Int).
diag! {
E.Database.Connection.TIMEOUT: {
message: "Database connection timed out",
'CR 'Pub description: "Service temporarily unavailable. Please try again.",
'CR 'Dev description: "Connection pool exhausted. Check max_connections config.",
'CR 'Int description: "Pool timeout at 30s. Consider scaling RDS instance.",
'R role: "Internal", // Default role for this diagnostic
}
}# Compile-Time Validation
Waddling Errors follows a "Fail Fast" philosophy. The diag! macro can validate your error definitions at compile time, catching typos and inconsistencies before they reach runtime.
Strict Mode
Use the strict(...) directive to enable specific checks.
diag! {
// Enable all checks
strict(sequence, primary, component, naming, duplicates),
E.Auth.Token.EXPIRED: {
message: "Token expired",
}
}Validation Modes
| Mode | What it Checks |
|---|---|
| sequence | Ensures EXPIRED exists in sequence! |
| primary | Ensures Token exists in primary! |
| component | Ensures Auth exists in component! |
| naming | Enforces UPPER_SNAKE (Seq) and PascalCase (Comp/Prim) |
| duplicates | Prevents defining E.Auth.Token.EXPIRED twice |
Best Practices
- Production: Use full strict mode.
strict(sequence, primary, component, naming, duplicates) - Prototyping: Use minimal checks.
strict(sequence) or relaxed
# Hashing & Compact IDs
Part 5 of the error structure is the Compact ID. It is a deterministic, 5-character hash derived from the structured code.
E.AUTH.TOKEN.001βxxHash3 + Base62βXy8QzWDP Conformance
By default, this crate follows the Waddling Diagnostic Protocol (WDP) spec:
- Seed:
wdp-v1(0x000031762D706477) - Algorithm: xxHash3
- Normalization: ASCII uppercase (deterministic, locale-independent)
- Alphabet: Base62 (0-9, A-Z, a-z)
Hashing is computed at compile time by the procedural macros. No runtime dependency or feature flag neededβhashes are embedded as static strings in your binary.
// Hashes are always available
let error = E_AUTH_TOKEN_EXPIRED;
println!("Hash: {}", error.hash); // Compile-time computed# Security & Roles
Waddling Errors includes a robust role-based visibility system for documentation. This ensures that sensitive implementation details (like internal file paths or administrative endpoints) are never exposed in public documentation, while still providing full transparency to your internal team.
The "Secure by Default" Philosophy
We believe security shouldn't be an afterthought. By default, all component locations are treated as Internal. You must explicitly opt-in to make a location Public. This prevents accidental leakage of internal file structures.
Role Hierarchy
- Public: Safe for everyone (customers, external partners). Shows
examples/only. - Developer: For contributors and team members. Adds debugging utilities and deeper context.
- Internal: (Default) For the core team/SRE. Shows everything, including secret rotation logic and admin handlers.
Tracking Component Locations
The #[in_component] macro automatically registers the file path of the module it decorates. Use the role parameter to control visibility.
// β INTERNAL (Default): Secure! Hidden from public docs.
#[in_component(Auth)]
mod jwt_signer {
// ...
}
// β
PUBLIC: Explicitly marked. Safe for public docs.
#[in_component(Auth, role = public)]
mod auth_example {
// ...
}Generating Role-Specific Docs
When generating documentation, the registry filters content based on the target role.
// Generates 3 separate files:
// 1. myapp-pub.json (Public only)
// 2. myapp-dev.json (Public + Developer)
// 3. myapp-int.json (Everything)
registry.render_all_roles(
vec![Box::new(JsonRenderer)],
"target/doc"
)?;# Integration
waddling-errors is designed to work seamlessly with the Rust ecosystem, including popular libraries like thiserror and anyhow.
Using with thiserror
You can map structured waddling error codes to your thiserror enums to get the best of both worlds: ergonomic error handling and structured diagnostics.
use thiserror::Error;
#[derive(Error, Debug)]
pub enum AppError {
#[error("{code}: {message}")]
Auth {
message: String,
code: String,
#[source]
source: Option<Box<dyn std::error::Error + Send + Sync>>,
},
}
impl AppError {
pub fn from_diag<D: waddling_errors::DiagnosticCode>(diag: &D, message: String) -> Self {
Self::Auth {
message,
code: diag.runtime.code.to_string(), // e.g., "E.Auth.Token.001"
source: None,
}
}
}Using with anyhow
Propagate your structured errors easily with anyhow::Result. The context mechanism preserves the chain of failure.
use anyhow::{Context, Result};
fn authenticate_user(token: &str) -> Result<User> {
if token.is_empty() {
// Convert waddling error to AppError, then auto-convert to anyhow::Error
return Err(AppError::from_diag(
&E_AUTH_TOKEN_MISSING,
"Authentication token is missing".to_string(),
).into());
}
// ...
Ok(user)
}# Embedded & no_std
waddling-errors is verified to work on real embedded hardware. The compile-time hash computation means zero runtime overhead on resource-constrained devices.
β Hardware Verified
These examples have been built, flashed, and tested on real hardwareβnot just "should work in theory."
Supported Architectures
| Board | Architecture | Target | Flash Usage |
|---|---|---|---|
| Raspberry Pi Pico RP2040 | ARM Cortex-M0+ | thumbv6m-none-eabi | ~24 KB |
| XIAO ESP32-S3 Seeed Studio | Xtensa LX7 | xtensa-esp32s3-none-elf | ~82 KB |
Why It Matters
The key insight is that hashes are computed at compile time by procedural macros. On embedded devices, you get:
- Zero runtime hash computation β hashes are
'static strliterals - No xxhash dependency in binary β only the proc-macro crate uses it
- Minimal RAM usage β error metadata lives in flash
- Same API everywhere β desktop and embedded code are identical
Raspberry Pi Pico Example
A minimal Pico example with USB serial output showing diagnostic codes and hashes:
#![no_std]
#![no_main]
extern crate alloc;
use waddling_errors::{component, diag, primary, sequence, setup};
// Define your error domain (same syntax as desktop!)
component! {
pub enum Component {
Sensor { docs: "Hardware sensors" },
}
}
primary! {
pub enum Primary {
Temperature { description: "Temperature readings" },
}
}
sequence! {
TIMEOUT(24) { description: "Operation timed out" },
}
setup! {
components = crate::components,
primaries = crate::primaries,
sequences = crate::sequences,
}
diag! {
relaxed,
E.Sensor.Temperature.TIMEOUT: {
message: "Sensor read timed out after {{ms}}ms",
fields: [ms],
},
}
// In your main loop:
// println!("Code: {}", E_SENSOR_TEMPERATURE_TIMEOUT.code);
// println!("Hash: {}", E_SENSOR_TEMPERATURE_TIMEOUT.hash); // e.g., "AgFGM"Cargo.toml for Embedded
[dependencies]
# no_std with macros and metadata
waddling-errors = { version = "0.7", default-features = false, features = ["macros", "metadata"] }
# Allocator (required for string formatting)
embedded-alloc = "0.6"ESP32-S3 Notes
For Xtensa targets (ESP32, ESP32-S2, ESP32-S3), you need the ESP Rust toolchain:
# Install ESP toolchain
cargo install espup
espup install
# In .cargo/config.toml - IMPORTANT: include alloc!
[unstable]
build-std = ["core", "alloc"]π Complete Examples
Full working examples with build instructions are available in the repository:
- examples/pico-minimal β Raspberry Pi Pico with USB serial
- examples/esp32s3-waddle β XIAO ESP32-S3 Sense
Sample Output
Here's actual serial output from the ESP32-S3 example:
INFO - Diagnostic: E.Network.Wifi.NOT_FOUND
INFO - Code: E.Network.Wifi.NOT_FOUND
INFO - Hash: lmPCy
INFO - Message: WiFi network '{{ssid}}' not found
INFO - Diagnostic: W.Network.Wifi.TIMEOUT
INFO - Code: W.Network.Wifi.TIMEOUT
INFO - Hash: Xh8uB
INFO - ========================================
INFO - Compile-time computed hashes work!
INFO - Running on Xtensa (ESP32-S3)!
INFO - ========================================# Advanced Features
Unlock the full potential of `waddling-errors` with custom renderers, global configuration, and advanced filtering capabilities.
Global Configuration
Configure documentation generation globally via Cargo.toml or environment variables, avoiding hardcoded paths in your code.
# Cargo.toml
[package.metadata.waddling-errors]
doc_formats = ["json", "html"]
doc_output_dir = "target/doc"// In your main.rs
registry.render_with_global_config()?;Custom Renderers
Implement the Renderer trait to generate documentation in any format (XML, Markdown, CSV, etc.). Use the generic syntax in the diag! macro to auto-register diagnostics for your custom format.
// Define diagnostics for specific formats
diag! {
<json, xml>, // Auto-registers for JSON and your custom XML renderer
E.Payment.Stripe.WEBHOOK_INVALID: {
message: "Stripe webhook signature verification failed",
}
}Format Filtering
Control which errors appear in which documentation formats. Useful for keeping internal debug errors out of public-facing HTML docs.
diag! {
<json>, // Only appears in JSON output (e.g. internal API)
E.INTERNAL.DEBUG: { ... },
}
diag! {
<html>, // Only appears in HTML output (e.g. user guide)
E.USER.GUIDE: { ... },
}Multiple Namespaces
Large systems can split errors into namespaces (e.g., `auth_service`, `payment_service`). Each namespace generates its own independent catalog.
diag! {
namespace: "auth_service",
<json>,
E.Auth.Login.FAILED: { ... }
}Generating Documentation
Use the DocRegistry to collect diagnostics and generate HTML/JSON catalogs. The auto-register feature automatically populates the registry.
use waddling_errors::prelude::*;
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Get the global registry (auto-populated by <json, html> markers)
let registry = waddling_errors::doc_generator::registry();
// Generate for all roles (Public, Developer, Internal)
registry.render_all_roles(
vec![
Box::new(waddling_errors::doc_generator::HtmlRenderer),
Box::new(waddling_errors::doc_generator::JsonRenderer),
],
"target/doc"
)?;
// Generates files like:
// - target/doc/MyProject-pub.html
// - target/doc/MyProject-dev.html
// - target/doc/MyProject-int.html
// - target/doc/MyProject-pub.json
// - target/doc/MyProject-dev.json
// - target/doc/MyProject-int.json
Ok(())
}Interactive HTML Features
Generated HTML catalogs include powerful filtering and search:
- Severity filtering: Show only Errors, Warnings, etc.
- Category filtering: Filter by Component or Primary
- Tag filtering: Search by custom tags
- Advanced query builder: Complex queries like "E.Auth.* with tag:security"
- Hash search: Look up errors by their 5-character Compact ID
- CSP-compliant: No inline scripts, works in strict security contexts
# Macro API Reference
A comprehensive reference for all procedural macros provided by waddling-errors-macros.
1. setup!
Required. Must be called once before using diag!. Creates a hidden __wd_paths module that tells the diag! macro where to find your components, primaries, and sequences at compile time.
// At crate root (lib.rs or main.rs)
use waddling_errors_macros::setup;
setup! {
components = crate::components, // Can be ANY module path!
primaries = crate::primaries,
sequences = crate::sequences,
}
// Example with custom module names:
setup! {
components = crate::auth, // Your module names here
primaries = crate::errors,
sequences = crate::codes,
}
// Now diag! can resolve "Auth", "Token", "EXPIRED" automatically!Why is this needed? Rust proc macros can't access your crate's module tree directly. The setup! macro generates a re-export module that diag!uses to validate components/primaries/sequences exist at compile time.
2. component!
Defines system components. Supports metadata tags and versioning info.
component! {
Auth {
docs: "Authentication system",
tags: ["security", "core"],
introduced: "1.0.0",
}
}3. primary!
Defines categories within components.
primary! {
Token {
docs: "JWT and Session Tokens",
related: ["Session"], // Links to 'Session' primary
}
}4. sequence!
Defines numeric codes (001-999). Supports importing from other modules.
sequence! {
// Import shared sequences
use crate::common::{MISSING, INVALID};
// Define local sequence
TIMEOUT(17) {
description: "Operation timed out",
typical_severity: "Error",
}
}5. diag!
The core macro. Defines the complete diagnostic with message templates, fields, and visibility markers.
diag! {
<json, html>, // Output formats
E.Auth.Token.EXPIRED: {
message: "Token expired at {{time}}",
fields: [time],
// Visibility Markers:
// 'C = Compile-time docs only
// 'R = Runtime binary only
// 'CR = Both (Default)
// Role Markers:
// 'Pub = Public Audience
// 'Dev = Developer Audience
// 'Int = Internal Audience
'CR 'Pub description: "Please log in again.",
'CR 'Dev description: "JWT exp claim < now.",
'C introduced: "1.0.0",
'R tags: ["auth"],
}
}6. component_location!
Marks a file as containing code for a specific component. Enables location tracking in generated documentation. Unlike #[in_component], this doesn't require wrapping code in a module.
use waddling_errors_macros::component_location;
// Mark file as Auth component (default: internal role)
component_location!(Auth);
// With explicit role for documentation visibility
component_location!(Auth, role = public);
component_location!(Database, role = developer);
component_location!(Cache, role = internal);Roles: public (shown to everyone), developer (devs + internal),internal (team only, default). With auto-register feature, locations are registered automatically at startup.
7. #[in_component]
Attribute macro that wraps a module to associate it with a component. Use when you want component association tied to a specific module scope.
use waddling_errors_macros::in_component;
// Default: internal role
#[in_component(Auth)]
mod auth_impl {
// Implementation code...
}
// With explicit role
#[in_component(Auth, role = public)]
mod auth_example {
//! Example code for public documentation
}
#[in_component(Database, role = developer)]
mod db_debug {
// Developer debugging utilities
}Note: Unlike component_location!, #[in_component] requires manual registration via the generated __register_component_location(&mut registry) function.