gerr

package module
v1.2.1 Latest Latest
Warning

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

Go to latest
Published: Mar 15, 2026 License: MIT Imports: 11 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

Library — add gerr to your project:

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

CLI (gerr gen) — install the code generator:

go install github.com/dinopuguh/gerr/cmd/gerr@latest

Verify the install:

gerr gen -help

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.

Code generation (gerr gen)

gerr ships a CLI tool that generates type-safe error constructors and locale JSON files from a YAML schema.

Usage:

gerr gen [-schema <file.yaml>|-schema-url <url>] [-out <dir>] [-locales <dir>] [-lang <lang>]
gerr gen -schema - < errors.yaml   # read from stdin
Flag Default Description
-schema Path to YAML schema file, or - to read from stdin
-schema-url URL to fetch YAML schema from (e.g. raw GitHub URL)
-out . Output directory for generated source files
-locales locales Output directory for locale JSON files
-lang go Target language (go, typescript)

One of -schema or -schema-url is required.

Schema format (example/gen/errors.yaml):

package: apierrors

validators:
  required:
    en: "The field '{{.Field}}' is required"
    id: "Kolom '{{.Field}}' wajib diisi"
  email:
    en: "The field '{{.Field}}' must be a valid email address"
    id: "Kolom '{{.Field}}' harus berupa alamat email yang valid"
  min:
    en: "The field '{{.Field}}' must be at least {{.Param}} characters"
    id: "Kolom '{{.Field}}' minimal {{.Param}} karakter"

errors:
  - name: UserNotFound
    code: USER-001
    key: error.user_not_found
    http: 404
    args: [username]
    messages:
      en: "User '{{.username}}' not found"
      id: "Pengguna '{{.username}}' tidak ditemukan"

  - name: Unauthorized
    code: AUTH-001
    key: error.unauthorized
    http: 401
    messages:
      en: "You are not authorized to perform this action"
      id: "Anda tidak memiliki izin untuk melakukan tindakan ini"

  - name: ServiceUnavailable
    code: SVC-001
    key: error.service_unavailable
    http: 503
    retryable: true   # explicit override; omit to use the default
    messages:
      en: "Service is temporarily unavailable"

The validators section is optional. When present, gerr gen merges validation.<tag> keys into the locale files automatically — no manual locale files needed for validator errors.

retryable field:

Condition Default
HTTP 5xx true
HTTP 4xx false
explicit retryable: true/false overrides the default

Omit the field to apply the default. Set it explicitly only when you need to deviate (e.g. a 4xx that the client should retry, or a 5xx that it should not).

Run the generator:

gerr gen -schema example/gen/errors.yaml -out example/gen -locales example/gen/locales

Generated Go output (example/gen/errors.go):

// Code generated by gerr gen from errors.yaml. DO NOT EDIT.
package apierrors

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

const (
    CodeUserNotFound = "USER-001"
    CodeUnauthorized = "AUTH-001"
)

func UserNotFound(username string, opts ...gerr.Option) error {
    return gerr.New(CodeUserNotFound, "error.user_not_found", http.StatusNotFound,
        append([]gerr.Option{gerr.WithArgs(map[string]any{"username": username}), gerr.WithRetryable(false)}, opts...)...,
    )
}

func Unauthorized(opts ...gerr.Option) error {
    return gerr.New(CodeUnauthorized, "error.unauthorized", http.StatusUnauthorized,
        append([]gerr.Option{gerr.WithRetryable(false)}, opts...)...,
    )
}

gerr.WithRetryable is always emitted — false for 4xx, true for 5xx — unless overridden in the schema.

The generator also writes or merges locale JSON files (one per language tag found in messages) into the -locales directory. Existing keys are preserved; schema keys are added or overwritten.

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 InitBundle added in v1.0.2

func InitBundle(defaultLang language.Tag, files ...string) error

InitBundle creates an i18n.Bundle with the given default language, loads the provided locale files, and sets the result as the package-level bundle.

This is the recommended single call to set up i18n at application startup. Clients are responsible for providing all locale files, including translations for any message keys used by external packages (e.g. errcodes).

Example:

gerr.InitBundle(language.English, "locales/en.json", "locales/id.json")

func LoadBundleFromFiles

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

LoadBundleFromFiles creates an i18n.Bundle with the given defaultLang (e.g. "en") and loads all provided paths. Each path may be a file or a directory; directories are walked recursively and all .json, .yaml, and .yml files are loaded.

Example:

bundle, err := LoadBundleFromFiles(language.English, "locales/")
bundle, err := LoadBundleFromFiles(language.English, "locales/en.json", "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
cmd
gerr command
gerr is the CLI tool for the gerr library.
gerr is the CLI tool for the gerr library.
gen
Code generated by gerr gen from errors.yaml.
Code generated by gerr gen from errors.yaml.

Jump to

Keyboard shortcuts

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