gerr

package module
v1.0.1 Latest Latest
Warning

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

Go to latest
Published: Jan 10, 2026 License: MIT Imports: 10 Imported by: 0

README

gerr

gerr is a small Go library that provides a structured error wrapper with:

  • machine-readable error Code
  • i18n-aware MessageKey and template Args
  • arbitrary Meta and Details
  • HTTP Status and Retryable flag
  • seamless wrapping of underlying error values (supports errors.Is / errors.As)
  • helpers to convert validator/v10 validation errors (uses the first validation error)
  • zerolog integration via MarshalZerologObject for structured logging

Module path: github.com/dinopuguh/gerr

Installation

To add gerr to your project (using Go modules):

go get github.com/dinopuguh/gerr@latest
go mod tidy

Usage (quick start)

  1. Load locales and set the package bundle at application startup:
bundle, err := gerr.LoadBundleFromFiles("en", "example/locales/en.json", "example/locales/id.json", "example/locales/ar.json")
if err != nil {
    // handle error
}
gerr.SetBundle(bundle)
  1. Create an error and obtain a localized user message:
rawErr := gerr.New("USER-001", "error.user_not_found", 404,
    gerr.WithArgs(map[string]interface{}{"username": "alice"}),
)
ge, _ := gerr.AsError(rawErr)
userMsg, _ := ge.UserMessage("en")
fmt.Println(userMsg)
  1. Wrap a lower-level error and log it with zerolog:
raw := gerr.Wrap(fmt.Errorf("db: fail"), "GENERIC-001", "error.unavailable", 503)
log.Error().Err(raw).Msg("operation failed")

Overview

gerr helps you:

  • Create consistent, machine-friendly error objects for your services.
  • Keep user-facing messages in i18n resource files (owned by your application).
  • Convert validator errors into an error that can be localized.
  • Attach HTTP-related info (status, retryable) to errors so HTTP handlers can map errors to responses.
  • Log errors as structured objects (zerolog).

Design highlights

  • Code (string): machine-readable code like GENERIC-001 or USER-001.
  • MessageKey (string): i18n key used to fetch a localized user message (e.g. error.user_not_found).
  • Args (map[string]any): arguments for message templates (e.g. {"username":"alice"}).
  • Details ([]string): developer details or extra human info.
  • Meta (map[string]any): arbitrary metadata (e.g., field error details).
  • Status (int): HTTP status code; defaulting behavior (e.g. validation -> 400).
  • Retryable (bool): indicates whether the client should retry (default true for 5xx).
  • Err (error): wrapped underlying error; Unwrap() is supported.

Key runtime decisions

  • Constructors (New / Wrap) return the built-in error interface. Use gerr.AsError(err) to obtain the richer API (accessors, localization, fluent methods).
  • The package exposes Option helpers to configure Args, Details, Meta, and Retryable at construction time.
  • A package-level i18n bundle is used by default for UserMessage / Localize calls; set it once with gerr.SetBundle(...) at startup. You can still localize with a specific bundle using LocalizeMessage(...).

i18n

  • Integration: github.com/nicksnyder/go-i18n/v2/i18n + golang.org/x/text/language.
  • Best practice: the client application owns the localization message files for all locales. The library provides LoadBundleFromFiles to help load them.
  • Example locale files are under example/locales/ to demonstrate expected keys and pluralization; replace or extend them in your application.

Where to put your locale files

Place your message files anywhere your app prefers. The example uses example/locales/. Example file content (shows plural forms):

{
  "validation.required": {
    "id": "validation.required",
    "other": "The field '{{.Field}}' is required"
  },
  "error.user_not_found": {
    "id": "error.user_not_found",
    "other": "User '{{.username}}' not found"
  },
  "items.count": {
    "id": "items.count",
    "one": "You have one item",
    "other": "You have {{.Count}} items"
  }
}

Pluralization and Count detection

When you localize messages that require plural forms (e.g. items.count), provide a numeric Count in the Args when constructing the error. The library helper LocalizeMessage automatically detects Args["Count"] (or args["count"]) and passes it as PluralCount to go-i18n so the correct plural form is selected.

Usage examples

Notes before examples

  • New and Wrap return error. Use gerr.AsError(err) to get the richer API back (it will return a *gerr instance even for unknown errors — AsError wraps unknown errors into a UNKNOWN-000 gerr so callers can always inspect code/status/meta).
  • Set the package i18n bundle at startup: gerr.SetBundle(bundle).
  • If you prefer to localize using a specific bundle for a one-off, use LocalizeMessage(bundle, lang, key, args).
  1. Basic creation and localization
  • Create an error with HTTP status and options. Then use gerr.AsError to access details and user message.
package main

import (
  "fmt"
  "net/http"
  "github.com/dinopuguh/gerr"
)

func main() {
  bundle, _ := gerr.LoadBundleFromFiles("en", "example/locales/en.json", "example/locales/id.json")
  gerr.SetBundle(bundle)

  // New returns an error (built-in). Use WithArgs via options.
  rawErr := gerr.New("USER-001", "error.user_not_found", http.StatusNotFound,
    gerr.WithArgs(map[string]interface{}{"username": "alice"}),
  )

  // Obtain the richer gerr object
  ge, ok := gerr.AsError(rawErr)
  if ok {
    userMsg, _ := ge.UserMessage("en") // localized string using package bundle
    fmt.Println("EN:", userMsg)
  }
}
  1. Wrapping an underlying error and logging
  • Wrap a lower-level error and log it as a structured object with zerolog. *gerr implements MarshalZerologObject.
package main

import (
  "errors"
  "net/http"
  "github.com/dinopuguh/gerr"
  "github.com/rs/zerolog/log"
)

func main() {
  dbErr := errors.New("sql: no rows in result set")
  rawWrapped := gerr.Wrap(dbErr, "GENERIC-001", "error.unavailable", http.StatusServiceUnavailable,
    gerr.WithArgs(map[string]interface{}{"Resource": "payment"}),
    gerr.WithRetryable(true),
  )

  wrapped, _ := gerr.AsError(rawWrapped)
  userMsg, _ := wrapped.UserMessage("en")
  log.Error().
    Str("operation", "query_db").
    Str("user_message", userMsg).
    Object("error", wrapped). // triggers MarshalZerologObject
    Msg("database query failed")
}
  1. Validator error conversion (first error)
  • Convert validator/v10 errors to gerr (first FieldError) and localize.
package main

import (
  "fmt"
  "github.com/dinopuguh/gerr"
  "github.com/go-playground/validator/v10"
)

func main() {
  validate := validator.New()
  p := struct{ Email string `validate:"required,email"` }{Email: "bad"}
  if err := validate.Struct(p); err != nil {
    vErr := gerr.FromValidatorErr(err) // returns error
    if ge, ok := gerr.AsError(vErr); ok {
      msg, _ := ge.UserMessage("en") // ge.MessageKey = "validation.<tag>"
      fmt.Println("Validation message:", msg)
    }
  }
}
  1. Multiple machine codes mapping to the same message key
  • The library separates Code (machine) and MessageKey (user message). The mapping is controlled by your application.
raw1 := gerr.New("GENERIC-002", "error.unavailable", 503, gerr.WithArgs(map[string]interface{}{"Resource":"service"}))
raw2 := gerr.New("SERVICE-003", "error.unavailable", 503, gerr.WithArgs(map[string]interface{}{"Resource":"auth"}))

e1, _ := gerr.AsError(raw1)
e2, _ := gerr.AsError(raw2)

fmt.Println("Code:", e1.Code(), "Message:", e1.UserMessage("en"))
fmt.Println("Code:", e2.Code(), "Message:", e2.UserMessage("en"))

Zerolog integration (structured logs)

  • *gerr implements zerolog.LogObjectMarshaler via MarshalZerologObject.
  • You can include the error as an object in the event: log.Error().Object("error", ge).Msg("...").
  • The marshaler emits: code, message_key, status, retryable, cause, args, meta, and details.

API summary (current)

  • Constructors (return built-in error):

    • func New(code, messageKey string, httpCode int, opts ...Option) error
    • func Wrap(err error, code, messageKey string, httpCode int, opts ...Option) error
  • Options:

    • type Option func(*gerr)
    • func WithArgs(args map[string]any) Option
    • func WithDetails(details ...string) Option
    • func WithMeta(key string, value any) Option
    • func WithRetryable(retryable bool) Option
  • Accessing gerr features:

    • func AsError(err error) (*gerr, bool) — always returns a *gerr for non-nil input (unknown errors are wrapped into UNKNOWN-000).
    • func GetCode(err error) (string, bool)
    • func GetMessageKey(err error) (string, bool)
  • *gerr accessors:

    • func (g *gerr) Code() string
    • func (g *gerr) MessageKey() string
    • func (g *gerr) Args() map[string]any
    • func (g *gerr) Meta() map[string]any
    • func (g *gerr) Status() int
    • func (g *gerr) Retryable() bool
    • func (g *gerr) Unwrap() error
    • func (g *gerr) Error() string // <Code> (<wrapped error>)
    • func (g *gerr) UserMessage(lang string) (string, error) // uses package bundle via SetBundle
  • i18n helpers:

    • func LoadBundleFromFiles(defaultLang string, files ...string) (*i18n.Bundle, error)
    • func SetBundle(b *i18n.Bundle)
    • func GetBundle() *i18n.Bundle
    • func LocalizeMessage(bundle *i18n.Bundle, lang string, key string, args map[string]any) (string, error)
    • func RegisterMessages(bundle *i18n.Bundle, lang string, messages map[string]string) error
    • func RegisterValidatorTagMessages(bundle *i18n.Bundle, lang string, tagMessages map[string]string) error
  • Validator conversion:

    • func FromValidatorErr(err error) error — converts validator errors into a gerr (first field error); returns the built-in error.

Best practices

  • Keep user-facing messages under your application's control in locale files.
  • Use Code for programmatic checks (monitoring, metrics, branching).
  • Use MessageKey + Args for user-facing messages and translations.
  • Prefer whole-sentence message templates for translators (avoid composing tokens across languages).
  • Set the package bundle at startup with SetBundle and call UserMessage to get localized strings.
  • For logging, include the structured *gerr object in zerolog events to capture rich context.

Contributing and license

This repository is intentionally minimal and intended to be adapted. If you want tests, additional helpers (e.g., a client-owned registry), or changes to defaults (e.g., unknown-code name or HTTP status mapping), open an issue or PR or ask for edits.

Module

Module path: github.com/dinopuguh/gerr

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func FromValidatorErr

func FromValidatorErr(err error) error

FromValidatorErr converts an error produced by github.com/go-playground/validator/v10 into a gerr error (returns the built-in error interface). Behavior:

  • If err is nil, returns nil.
  • If err is a validator.ValidationErrors (or contains it in its chain), this uses only the first FieldError to construct an error with:
  • Code = "validation.failed"
  • MessageKey = "validation.<tag>" (e.g. "validation.required")
  • HTTP status = 400
  • Retryable = false
  • Args and Meta populated with field/tag/param info
  • If err is not a validation error, returns nil.

func GetBundle

func GetBundle() *i18n.Bundle

GetBundle returns the currently-set package-level i18n bundle (may be nil).

func GetCode

func GetCode(err error) (string, bool)

GetCode extracts a machine-friendly code from any error chain, if present.

func GetMessageKey

func GetMessageKey(err error) (string, bool)

GetMessageKey extracts the MessageKey from any error chain, if present.

func GetUserMessage

func GetUserMessage(err error, lang string) (string, error)

GetUserMessage extracts the UserMessage from any error chain, if present.

func LoadBundleFromFiles

func LoadBundleFromFiles(defaultLang language.Tag, files ...string) (*i18n.Bundle, error)

LoadBundleFromFiles creates an i18n.Bundle with the given defaultLang (e.g. "en") and loads all provided message files. Supported file formats: JSON and YAML. The filenames' extensions (.json, .yaml, .yml) are used to determine the format.

Example:

bundle, err := LoadBundleFromFiles("en", "example/locales/en.json", "example/locales/id.json")

func New

func New(code, messageKey string, httpCode int, opts ...Option) error

New constructs a new error (built-in error interface) representing a gerr. Parameters:

  • code: machine-friendly code, e.g. "GENERIC-001" or "USER-001"
  • messageKey: i18n message key, e.g. "error.user_not_found"
  • httpCode: suggested HTTP status code
  • opts: optional Option helpers (WithArgs, WithMeta, WithRetryable)

The function returns the standard built-in error interface. Callers that need access to gerr-specific features (Localize, accessors) should use AsError / errors.As to obtain the exported Error interface.

func RegisterMessages

func RegisterMessages(bundle *i18n.Bundle, lang string, messages map[string]string) error

RegisterMessages registers arbitrary messageID->template pairs into the provided bundle for the given language. This helper is generic and can be used to register any message IDs.

Example:

RegisterMessages(bundle, "en", map[string]string{
  "error.unavailable": "Service '{{.Resource}}' is unavailable",
})

func RegisterValidatorTagMessages

func RegisterValidatorTagMessages(bundle *i18n.Bundle, lang string, tagMessages map[string]string) error

RegisterValidatorTagMessages registers a map of validator tag -> template text into the provided i18n.Bundle for the given language. Each entry will be registered under the message ID "validation.<tag>".

Example:

RegisterValidatorTagMessages(bundle, "en", map[string]string{
  "required": "{{.Field}} is required",
  "email": "{{.Field}} must be a valid email address",
})

func SetBundle

func SetBundle(b *i18n.Bundle) error

SetBundle sets the package-level i18n bundle used by Localize.

func ValidatorMessageKeyForFieldError

func ValidatorMessageKeyForFieldError(fe validator.FieldError) string

ValidatorMessageKeyForFieldError returns the message key that should be used for the provided FieldError. The convention used is "validation.<tag>". If the tag is empty, falls back to "validation.failed".

func Wrap

func Wrap(err error, code, messageKey string, httpCode int, opts ...Option) error

Wrap constructs a new error that wraps an underlying err. Other parameters are the same as New.

Types

type Gerr

type Gerr interface {
	Code() string
	MessageKey() string
	Args() map[string]any
	Metadata() map[string]any
	Status() int
	Retryable() bool
	Unwrap() error
	Error() string
	UserMessage(lang string) (string, error)
}

func AsError

func AsError(err error) (Gerr, bool)

AsError tries to extract our Error interface from an arbitrary error chain. It returns the Error interface and true on success.

type Metadata

type Metadata map[string]any

func (*Metadata) Add

func (m *Metadata) Add(key string, value any)

type Option

type Option func(*gerr)

Option configures a newly created gerr instance.

func WithArgs

func WithArgs(args map[string]any) Option

WithArgs sets the template args to be used for localization. It overwrites any existing args map.

func WithMetadata

func WithMetadata(metadata Metadata) Option

WithMetadata adds a metadata key/value pair. It appends the metadata to the existing metadata map.

func WithRetryable

func WithRetryable(retryable bool) Option

WithRetryable explicitly sets retryable flag (overrides HTTP-based default).

Directories

Path Synopsis

Jump to

Keyboard shortcuts

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