error-engine 0.1.0

A message catalog + presentation layer on top of thiserror and tracing for consistent error/warning/info rendering.
Documentation

error-engine

A shared Rust crate to centralize error, warning, and info presentation across personal projects. It does not replace thiserror or tracing — it sits on top of them as a message catalog + presentation layer.

Each project defines its errors as an enum with thiserror (re-exported by this crate) and implements the EngineDiagnostic trait. A TOML catalog file maps each diagnostic's stable code to a message template and an optional hint, so wording can be edited without touching Rust code.

Install

Not published to crates.io — add it as a path or git dependency:

[dependencies]
error-engine = { git = "https://github.com/<you>/error-engine" }

Enable the cli feature if you want colored terminal output (Engine::print):

[dependencies]
error-engine = { git = "https://github.com/<you>/error-engine", features = ["cli"] }

Usage

  1. Define your errors with thiserror, using error_engine::thiserror::Error so you don't need thiserror as a separate dependency:

    use error_engine::thiserror::Error;
    
    #[derive(Debug, Error)]
    enum AppError {
        #[error("config not found")]
        ConfigNotFound { path: String },
    }
    
  2. Implement EngineDiagnostic — a stable code(), a severity(), and optional context() key-value pairs to interpolate into the template:

    use error_engine::{EngineDiagnostic, Severity};
    
    impl EngineDiagnostic for AppError {
        fn code(&self) -> &'static str {
            match self {
                AppError::ConfigNotFound { .. } => "CONFIG_NOT_FOUND",
            }
        }
        fn severity(&self) -> Severity {
            Severity::Error
        }
        fn context(&self) -> Vec<(&'static str, String)> {
            match self {
                AppError::ConfigNotFound { path } => vec![("path", path.clone())],
            }
        }
    }
    
  3. Write a catalog TOML file at your project's root, e.g. errors.toml:

    [CONFIG_NOT_FOUND]
    template = "Could not find the configuration file at {path}"
    hint = "Check that the path is correct, or create one with `myapp init`"
    
    [DB_CONN_FAILED]
    template = "Could not connect to the database: {reason}"
    
    • Each table key ([CODE]) must match exactly the code() returned by the corresponding EngineDiagnostic.
    • template is required, hint is optional.
    • {key} placeholders are substituted with values from context(). A placeholder with no matching value is left literal in the text — it never panics.
    • UNK-000 is reserved by the crate (see Failure behavior) and should not be reused as a project-defined code.
  4. Load the catalog and build an Engine:

    use error_engine::{Catalog, Engine};
    
    fn main() -> anyhow::Result<()> {
        tracing_subscriber::fmt::init();
    
        // Infallible: a missing/broken errors.toml never stops the app —
        // diagnostics will just render as UNK-000 until it's fixed.
        let catalog = Catalog::load_or_fallback("errors.toml");
        let engine = Engine::new(catalog);
    
        let err = AppError::ConfigNotFound { path: "/etc/app.toml".into() };
        engine.print(&err); // requires the "cli" feature
        engine.log(&err);
        Ok(())
    }
    

Run the bundled examples for a working end-to-end reference:

cargo run --example basic
cargo run --example cli --features cli

Output modes

Method Returns Use case
message() String GUI apps: plain text only, no logging, no color
log() nothing, goes to tracing Structured logging (files, services)
print() nothing, goes to stdout/stderr CLIs: colored output with hint (cli feature)

print() lives behind the cli feature flag so GUI/service projects don't pull in terminal-only dependencies (owo-colors).

Failure behavior (harmless by design)

Nothing about diagnostic rendering should ever panic or crash the application — a broken catalog or a typo'd code is a documentation problem, not a runtime crash. Two failure modes are unified under a single reserved code, UNK-000:

  1. The catalog itself failed to load (file missing, unreadable, or invalid TOML).
  2. The catalog loaded fine, but the requested code has no entry in it.

In both cases, rendering returns a message built around UNK-000 stating which of the two happened, instead of failing.

  • Catalog::load(path) — strict loader, returns Result<Catalog, EngineError>. Use at startup if you want the app to refuse to run without a valid catalog.
  • Catalog::load_or_fallback(path) — infallible loader, and the recommended entry point. A broken or missing catalog never panics; every diagnostic that would have failed instead renders as UNK-000 with an explanatory message.

Project structure

error-engine/
├── Cargo.toml
├── .cargo/
│   └── config.toml      # rustflags = ["-D", "warnings"]
├── src/
│   ├── lib.rs            # public re-exports (includes `pub use thiserror`)
│   ├── diagnostic.rs      # trait EngineDiagnostic + enum Severity
│   ├── catalog.rs         # TOML catalog loading/rendering + UNK-000 fallback
│   ├── engine.rs          # struct Engine: message() / log() / print()
│   └── error.rs           # crate's own internal errors (catalog loading)
├── examples/
│   ├── basic.rs           # minimal usage: message() + log()
│   ├── cli.rs              # usage with the `cli` feature: print()
│   └── errors.toml         # sample catalog used by both examples
└── tests/
    └── catalog.rs          # catalog loading/rendering tests

Testing

cargo test --all-features

Non-goals (v0.1)

  • No compile-time validation that codes used in the project exist in the catalog.
  • No #[derive(EngineDiagnostic)] macro.
  • No shared/inherited catalogs across projects (one catalog = one project).
  • No i18n (multiple languages per catalog).

See docs/error-engine-design.md for the full design rationale.