πŸ¦†

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:

Semantic (Human)
E.Auth.Token.001
β†’
Compact ID (Machine)
Xy8Qz
  • 1
    Severity

    One character (E, W, I...) indicating urgency.

  • 2
    Component

    High-level system module (e.g., Auth, Database).

  • 3
    Primary

    Logical grouping within a component (e.g., Token, Connection).

  • 4
    Sequence

    Specific numeric event identifier (e.g., 001).

  • 5
    Compact 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:

Cargo.tomltoml
[dependencies]
waddling-errors = "0.7"

Feature Flags

Control your footprint with granular feature flags.

Cargo.tomltoml
[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.

EnvironmentConfigRuntime Capabilities
Pure no_stddefault-features = falseβœ… Error code constants
βœ… Severity checking
βœ… Compile-time hashes
❌ String formatting
❌ Collections (Vec, HashMap)
no_std + allocDefaultβœ… Everything from pure no_std
βœ… String formatting
βœ… Procedural macros
βœ… Metadata collections
❌ File I/O
With stdfeatures = ["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

Cargo.tomltoml
// 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-errors

Or 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:

src/main.rsrust
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],
    }
}
FieldRequiredTypeDescription
messageYesstringTemplate with {{field}} placeholders
fieldsNoarrayNon-PII field names
piiNoarrayPII field names (use {{pii/field}})
severityNoidentOverride 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

ModeWhat it Checks
sequenceEnsures EXPIRED exists in sequence!
primaryEnsures Token exists in primary!
componentEnsures Auth exists in component!
namingEnforces UPPER_SNAKE (Seq) and PascalCase (Comp/Prim)
duplicatesPrevents 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β†’Xy8Qz

WDP 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

BoardArchitectureTargetFlash Usage
Raspberry Pi Pico
RP2040
ARM Cortex-M0+thumbv6m-none-eabi~24 KB
XIAO ESP32-S3
Seeed Studio
Xtensa LX7xtensa-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 str literals
  • 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

Cargo.tomltoml
[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:

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.tomltoml
# 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.

Theme Color
🌈 Auto Cycle
HUE: 239.0
CYCLING: ON