horus

package module
v1.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 8, 2026 License: GPL-3.0 Imports: 11 Imported by: 12

README

horus

License Documentation Go Report Card Release

Overview

horus is a Go error‐handling toolkit that does more than “return an error”

horus captures the what (operation), why (user‐friendly message), where (stack trace), and how (underlying cause and arbitrary key/value details), then lets you:

  • Wrap and re‐wrap errors with layered context
  • Seamlessly propagate or bail out with CheckErr (colored/JSON + configurable exit codes)
  • Plug into any CLI, HTTP handler, or logger with zero ceremony
  • Whether you’re building a command‐line tool, a microservice, or a big data pipeline, horus ensures nothing gets lost in translation when things go sideways

Features

Context-Rich Errors

Create Herror instances via NewHerror, NewCategorizedHerror, Wrap or WithDetail that carry:

  • Operation name (Op)
  • Human-readable message (Message)
  • Category tag (Category)
  • Arbitrary details map (Details)
  • Full stack trace
Error Propagation & Inspection
  • PropagateErr for idiomatic upstream wrapping
  • RootCause(err) to peel back nested failures
  • Helpers like AsHerror, IsHerror, Operation, UserMessage, GetDetail, Category, StackTrace
err := doSomething()
// only if err != nil it gets wrapped
return horus.PropagateErr("DoSomething", "SERVICE", "failed", err, nil)
Flexible Formatting
  • JSONFormatter for structured logs
  • PseudoJSONFormatter for aligned, colorized tables in your terminal
  • PlainFormatter or SimpleColoredFormatter for minimal output
Check & Exit
  • CheckErr(err, opts...) writes formatted error to your choice of io.Writer and exits with a customizable code
  • Built-in overrides: writer, formatter, exit code, operation, category, message, details
horus.CheckErr(err)               // default: colored table + os.Exit(1)
horus.CheckErr(err, horus.WithWriter(os.Stdout), horus.WithExitCode(42))
Not-Found Hooks
  • LogNotFound / NullAction implement NotFoundAction for pluggable “resource missing” behaviors
  • Fully testable via WithLogWriter
act := horus.LogNotFound("cache miss")
resolved, err := act("user:123")
// resolved==false, err==nil
Test Utilities
  • CollectingError (implements io.Writer + error) to capture and inspect output in tests
  • Easy use of WithWriter(buf) to drive deterministic output
Panic Integration
  • Panic(op, msg) logs a colored panic banner, captures a stack, then panics with a full Herror payload

Quickstart

package main

import (
  "errors"
  "fmt"

  "github.com/DanielRivasMD/horus"
)

func loadConfig(path string) error {
  // pretend this fails
  return errors.New("file not found")
}

func main() {
  err := loadConfig("/etc/app.cfg")
  if err != nil {
    // wrap with context, category, and detail
    wrapped := horus.NewCategorizedHerror(
      "LoadConfig",
      "IO_ERROR",
      "unable to load configuration",
      err,
      map[string]any{"path": "/etc/app.cfg"},
    )
    // print a pretty, colored table to stderr and exit
    horus.CheckErr(wrapped)
  }

  fmt.Println("config loaded")
}

Installation

Language-Specific
Language Command
Go go get github.com/DanielRivasMD/horus@latest

Usage

import "github.com/DanielRivasMD/horus"
Error Handling Integration

The horus error-handling library provides a set of powerful functions to wrap, propagate, log, and format errors across your application. Here’s how you can leverage these functions at various layers:

1. Lower-Level Functions

Wrap errors as soon as they occur

For example, when reading a configuration file:

package fileutils

import (
  "fmt"
  "os"

  "github.com/DanielRivasMD/horus"
)

// ReadConfig tries to read a JSON config from disk.
// On failure it wraps the underlying error with full context, category and details.
func ReadConfig(path string) ([]byte, error) {
  data, err := os.ReadFile(path)
  if err != nil {
    return nil, horus.PropagateErr(
      "ReadConfig",                              // Op
      "IO_ERROR",                                // Category
      fmt.Sprintf("unable to load config"),      // Message
      err,                                       // underlying error
      map[string]any{                            // Details
        "path": path,
      },
    )
  }
  return data, nil
}

What this does:

  • Uses Go 1.16+’s os.ReadFile instead of the deprecated ioutil
  • Always returns nil or a rich *Herror, never a raw error
  • Stamps on:
    • Op = "ReadConfig"
    • Category = "IO_ERROR"
    • Message = a user-friendly "unable to load config"
    • Details = {"path": path}
    • Stack trace (captured at the call site)

You’ll now get output like:

Op       ReadConfig,
Message  unable to load config,
Err      open /etc/app.cfg: no such file or directory,
path     /etc/app.cfg,
Category IO_ERROR,

Stack
  fileutils.ReadConfig()
    /Users/.../fileutils/config.go:12
  main.main()
    /Users/.../cmd/app/main.go:23
  runtime.main()
    /usr/local/go/src/runtime/proc.go:250
  ...

and if you prefer JSON:

horus.CheckErr(err, horus.WithFormatter(horus.JSONFormatter))

will emit something like:

{
  "Op": "ReadConfig",
  "Message": "unable to load config",
  "Err": "open /etc/app.cfg: no such file or directory",
  "Details": { "path": "/etc/app.cfg" },
  "Category": "IO_ERROR",
  "Stack": [ ... ]
}
2. Business Logic Functions

When higher-level functions catch errors from lower‐level routines, add domain‐specific context with PropagateErr (or WithDetail) so every layer contributes its own clues:

package business

import (
  "github.com/your_module/fileutils"
  "github.com/DanielRivasMD/horus"
)

// LoadAndProcessConfig orchestrates reading + validating your config.
// Any I/O or parse failures get wrapped with step-specific context.
func LoadAndProcessConfig(configPath string) error {
  // 1. Call the fileutils helper
  data, err := fileutils.ReadConfig(configPath)
  if err != nil {
    // PropagateErr merges the underlying category/details and stamps on new ones.
    return horus.PropagateErr(
      "LoadAndProcessConfig",           // operation name
      "CONFIG_ERROR",                   // business category
      "unable to load application config", // user-friendly message
      err,                              // the error from ReadConfig
      map[string]any{                   // extra details
        "path":   configPath,
        "service": "business",
      },
    )
  }

  // 2. (Optional) add validation context
  if len(data) == 0 {
    // NewHerror creates a fresh Herror; WithDetail would wrap an existing one.
    return horus.NewHerror(
      "LoadAndProcessConfig",
      "config data is empty",
      nil,
      map[string]any{"path": configPath},
    )
  }

  // 3. process the config...
  return nil
}
  • PropagateErr automatically carries forward any category or details from the lower layer (and merges the new payload)
  • We choose a “business” category ("CONFIG_ERROR") that’s orthogonal to the lower-level "IO_ERROR"
  • We include both path and our own service:"business" detail.
  • For purely business‐rule failures (like empty data), we use NewHerror to start a fresh error.
3. Centralized Error Reporting and Logging

At the top‐level ofthe app - usually in main() - CheckErr can be use as one‐stop fatal error handler:

  • Format the error (colored table by default) or JSON if you prefer
  • Register the error’s category in a global registry (for metrics/observability)
  • Exit with a configurable code (default: 1)
package main

import (
  "os"

  "github.com/DanielRivasMD/horus"
  "github.com/your_module/business"
)

func main() {
  // Run your business logic
  err := business.LoadAndProcessConfig("config.json")
  if err != nil {
    // Default: colored table → stderr, exit code 1
    horus.CheckErr(err)

    // Or JSON + code 2 + log to stdout:
    // horus.CheckErr(
    //   err,
    //   horus.WithFormatter(horus.JSONFormatter),
    //   horus.WithExitCode(2),
    //   horus.WithWriter(os.Stdout),
    // )
  }

  // Continue with normal execution...
}

Under the hood, CheckErr does:

  • RegisterError(err) – increments a counter for your error’s category
  • fmt.Fprintln(writer, formatter(err)) – prints your chosen format
  • exitFunc(code) – calls os.Exit(code) by default

This ensures that all unhandled, fatal errors flow through a consistent, observable pipeline

4. Integration with External Processes

For commands executed via external processes (e.g., running system commands), use functions like ExecCmd or CaptureExecCmd, horus:

  • Shows both ExecCmd (streams output) and CaptureExecCmd (buffers it)
  • Wraps errors with PropagateErr for context before calling CheckErr
  • Logs stdout/stderr details in your error’s Details map
package domovoi

import (
  "fmt"

  "github.com/DanielRivasMD/horus"
)

// ListDirectory runs `ls -la <path>` twice: once streamed, once captured.
// It returns an error if anything fails; caller can then CheckErr(err).
func ListDirectory(path string) error {
  // 1) Stream mode
  if err := ExecCmd("ls", "-la", path); err != nil {
    // ExecCmd already wraps in *Herror, but we can add our own op/category
    return horus.NewCategorizedHerror(
      "ListDirectory",                // Op
      "SYS_CMD",                      // Category
      fmt.Sprintf("ls -la %s", path), // Message
      err,
      nil, // no extra details here
    )
  }

  // 2) Capture mode
  stdout, stderr, err := CaptureExecCmd("ls", "-la", path)
  if err != nil {
    // We got a *Herror from CaptureExecCmd—merge in the captured output
    return horus.PropagateErr(
      "ListDirectory",
      "SYS_CMD",
      "failed to capture ls output",
      err,
      map[string]any{"stdout": stdout, "stderr": stderr},
    )
  }

  fmt.Println("=== STDOUT ===")
  fmt.Print(stdout)
  fmt.Println("=== STDERR ===")
  fmt.Print(stderr)
  return nil
}
  • ExecCmd(op, category, message, *exec.Cmd) runs and logs failures immediately
  • CaptureExecCmd returns (stdout, stderr string, err error)
  • We propagate errors with PropagateErr so our top‐level CheckErr shows the full story: operation, category, message, underlying cause, stdout, stderr, and stack trace
5. OS Level Operations

When wrapping system calls like os.Chdir, use Horus to enrich and propagate errors with full context:

package domovoi

import (
  "fmt"
  "os"

  "github.com/DanielRivasMD/horus"
)

// ChangeDirectory attempts to chdir into the given path.
// On failure it wraps the underlying os.Chdir error with operation,
// category, user-friendly message, and the path in Details.
func ChangeDirectory(path string) error {
  if err := os.Chdir(path); err != nil {
    return horus.PropagateErr(
      "ChangeDirectory",                               // Op
      "FS_ERROR",                                      // Category
      fmt.Sprintf("unable to change working directory"),// Message
      err,                                             // underlying error
      map[string]any{"path": path},                    // Details
    )
  }
  return nil
}
  • Op = "ChangeDirectory"
  • Category = "FS_ERROR"
  • Message = "unable to change working directory"
  • Details = {"path": "/some/dir"}
  • Stack trace captured at the call site

That way, any failure bubbles up as a full *Herror - complete with stack, category, and details—making your logs and CLI output immediately actionable

Development

Build from source:

git clone https://github.com/DanielRivasMD/horus
cd horus

Language-Specific Setup

Language Dev Dependencies Hot Reload
Go go >= 1.22 air (live reload)

License

Copyright (c) 2025

See the LICENSE file for license details.

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Category

func Category(err error) (string, bool)

Category returns the category associated with an Herror, if present.

func CheckEmpty added in v1.1.0

func CheckEmpty(val, errMsg string, opts ...checkOpt)

CheckEmpty will call CheckErr if the given string is empty.

val     – the string to validate
errMsg  – the error text to wrap (e.g. "`--script` is required")
opts    – any CheckErr options (WithOp, WithMessage, etc.)

func CheckErr

func CheckErr(err error, opts ...checkOpt)

CheckErr registers, wraps, formats and logs a fatal error via Horus. If err is non-nil it prints using the configured FormatterFunc, then exits.

func Details

func Details(err error) map[string]any

Details returns all details associated with an Herror, if present. Details returns all details associated with an Herror, or an empty (non-nil) map if there is none.

func FormatPanic

func FormatPanic(op, message string) string

FormatPanic returns a red-formatted panic message for the given operation and message.

func GetDetail

func GetDetail(err error, key string) (any, bool)

GetDetail returns a specific detail associated with an Herror, if present.

func GetErrorRegistry

func GetErrorRegistry() map[string]int

GetErrorRegistry returns a copy of the current error counts.

func IsHerror

func IsHerror(err error) bool

func JSONFormatter

func JSONFormatter(h *Herror) string

JSONFormatter generates a JSON representation of an Herror.

func Must added in v1.1.0

func Must[T any](v T, err error) T

Must returns the value v if err is nil; otherwise it calls CheckErr(err) and exits.

func NewCategorizedHerror

func NewCategorizedHerror(
	op, category, msg string,
	err error,
	details map[string]any,
) error

NewCategorizedHerror creates a new Herror with a category.

func NewHerror

func NewHerror(op, msg string, err error, details map[string]any) error

NewHerror creates a new Herror (no category).

func NewHerrorErrorf

func NewHerrorErrorf(op, fmtStr string, args ...any) error

TODO: set the API for one-liner here NewHerrorErrorf creates a new Herror with a formatted message.

func OneLineErr added in v1.2.0

func OneLineErr(er string) string

OneLineErr returns a bold, red version of the input string. Useful for displaying a single‑line error message without the full Herror structure.

func Operation

func Operation(err error) (string, bool)

Operation returns the operation associated with an Herror, if present.

func PlainFormatter

func PlainFormatter(h *Herror) string

func PropagateErr

func PropagateErr(
	op, category, message string,
	err error,
	details map[string]any,
) error

PropagateErr wraps a non-nil error in an Herror with the given context. If err is already an Herror, its Category and Details are optionally preserved (unless overridden) and merged with the new details. If err is nil, PropagateErr returns nil.

func PseudoJSONFormatter

func PseudoJSONFormatter(h *Herror) string

func RegisterError

func RegisterError(err error)

RegisterError increments the count for this error’s category.

func RootCause

func RootCause(err error) error

func RootCauseHelper

func RootCauseHelper(err error) error

func SimpleColoredFormatter

func SimpleColoredFormatter(h *Herror) string

SimpleColoredFormatter generates a colored representation of an Herror using the chalk library. You can extend this function to use different colors based on the error category.

func StackTrace

func StackTrace(err error) (string, bool)

StackTrace returns the formatted stack trace from an error if it's an Herror.

func UserMessage

func UserMessage(err error) (string, bool)

UserMessage returns the user-friendly message associated with an Herror, if present.

func WithCategory

func WithCategory(cat string) checkOpt

WithCategory overrides the error category.

func WithDetail

func WithDetail(err error, k string, v any) error

WithDetail adds a key-value detail to an existing Herror. If the error is not an Herror, a new Herror wrapping the original will be returned with the detail.

func WithDetails

func WithDetails(d map[string]any) checkOpt

WithDetails replaces the metadata map. If you want to merge instead of replace, read your existing details with Details(err) first.

func WithExitCode

func WithExitCode(code int) checkOpt

WithExitCode sets a custom exit code (defaults to 1).

func WithFormatter added in v1.1.0

func WithFormatter(f FormatterFunc) checkOpt

WithFormatter lets you choose any FormatterFunc (JSONFormatter, PlainFormatter, your own custom FormatterFunc, etc). Defaults to PseudoJSONFormatter.

func WithMessage

func WithMessage(msg string) checkOpt

WithMessage overrides the user‐facing message.

func WithOp

func WithOp(opName string) checkOpt

WithOp lets you override the operation name that CheckErr will wrap with.

func WithWriter

func WithWriter(w io.Writer) checkOpt

WithWriter redirects the error output (defaults to stderr).

func Wrap

func Wrap(err error, op, message string) error

Wrap wraps an existing error with additional context, capturing the stack trace.

Types

type CollectingError

type CollectingError struct {
	// contains filtered or unexported fields
}

CollectingError implements both io.Writer and error, accumulating writes into an internal buffer. It is safe for concurrent use.

func NewCollectingError

func NewCollectingError() *CollectingError

NewCollectingError returns an empty CollectingError.

func (*CollectingError) Bytes

func (ce *CollectingError) Bytes() []byte

Bytes returns a copy of the internal buffer's bytes. It is safe for concurrent calls.

func (*CollectingError) Error

func (ce *CollectingError) Error() string

Error returns the accumulated contents as a string. It is safe for concurrent calls.

func (*CollectingError) Reset

func (ce *CollectingError) Reset()

Reset clears the internal buffer, allowing reuse of the CollectingError. It is safe for concurrent calls.

func (*CollectingError) Write

func (ce *CollectingError) Write(p []byte) (n int, err error)

Write appends p to the internal buffer. It is safe for concurrent calls.

func (*CollectingError) WriteString

func (ce *CollectingError) WriteString(s string) (n int, err error)

WriteString appends s to the internal buffer. It is safe for concurrent calls.

type FormatterFunc

type FormatterFunc func(*Herror) string

FormatterFunc defines a function type for custom error formatting.

type HErrorer

type HErrorer interface {
	Herror() *Herror
}

type Herror

type Herror struct {
	Op       string         // The operation being performed (e.g., "read file", "connect db")
	Message  string         // A user-friendly message providing more context
	Err      error          // The underlying error, if any
	Details  map[string]any // Optional details for more specific context
	Category string         // Error category (e.g., validation, IO, etc.)
	Stack    []uintptr      // Stack trace captured at the time of error creation.
}

Herror represents a generalized error with added context.

func AsHerror

func AsHerror(err error) (*Herror, bool)

AsHerror tries to extract an Herror from the error chain.

func (*Herror) Error

func (e *Herror) Error() string

Error generates a human-readable representation of the error.

func (*Herror) Format

func (e *Herror) Format(f fmt.State, verb rune)

Format generates a custom representation of the error using a formatter function.

func (*Herror) HasStack

func (h *Herror) HasStack() bool

func (*Herror) Herror

func (h *Herror) Herror() *Herror

func (*Herror) MarshalJSON

func (h *Herror) MarshalJSON() ([]byte, error)

MarshalJSON ensures Err is emitted as its Error() string, not an object.

func (*Herror) StackTrace

func (e *Herror) StackTrace() string

StackTrace returns a formatted stack trace captured when the error was created.

func (*Herror) Unwrap

func (e *Herror) Unwrap() error

Unwrap provides access to the underlying error.

type LogNotFoundOption

type LogNotFoundOption func(*logNotFoundConfig)

LogNotFoundOption customizes how LogNotFound prints.

func WithLogWriter

func WithLogWriter(w io.Writer) LogNotFoundOption

WithLogWriter directs the warning message to a different io.Writer.

func WithNotFoundTemplate

func WithNotFoundTemplate(tmpl string) LogNotFoundOption

WithNotFoundTemplate lets you override the printf-style template.

type NotFoundAction

type NotFoundAction func(address string) (bool, error)

NotFoundAction is invoked when a lookup fails. It should return (resolved, err), where `resolved==false` means “we didn’t fix it,” and any non-nil error will be propagated.

func LogNotFound

func LogNotFound(contextMsg string, opts ...LogNotFoundOption) NotFoundAction

LogNotFound returns a NotFoundAction that logs a warning when a resource isn’t found. By default it prints to stderr in yellow:

Warning: Data address '...' not found. Context: ...

func NullAction

func NullAction(resolved bool) NotFoundAction

NullAction returns a NotFoundAction that does nothing. It simply returns false (meaning the missing resource remains unresolved) and no error. Use this when you want to provide a custom action that doesn't attempt any remediation.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL