goerr

package module
v1.7.0 Latest Latest
Warning

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

Go to latest
Published: Feb 23, 2026 License: MIT Imports: 5 Imported by: 2

README

goerr

goerr is a lightweight, structured error handling package for Go that provides error wrapping with context, metadata, and error kind classification for multi-transport applications (HTTP, gRPC, etc.).

Installation

go get -u github.com/tyrenix/goerr@latest

Features

  • Error Kind Classification: Categorize errors with semantic kinds (e.g., KindNotFound, KindInternal) for consistent mapping across different transports (HTTP, gRPC, WebSocket).
  • Contextual Error Wrapping: Add context at each layer while preserving the original error kind and metadata.
  • Structured Metadata: Attach custom fields to errors for rich logging and debugging.
  • Minimal User-Facing Messages: .Error() returns only the primary error code, safe for client display.
  • Detailed Logging: Use Details() or fmt.Printf("%v", err) to get the full error chain with context and fields.
  • Level-Scoped Metadata: Fields belong to the current error level; no implicit recursive lookup.
  • Go Compatible: Full support for errors.Is, errors.As via standard Unwrap() chaining.
  • Custom Formatting: Implements fmt.Formatter with %v (full details), %s (primary error), %q (quoted).

Quick Start

package main

import (
    "fmt"
    "github.com/tyrenix/goerr"
)

// Define error kinds for your application
var (
    KindNotFound = errors.New("not_found")
    KindInternal = errors.New("internal")
)

// Define domain errors
var (
    ErrUserNotFound = goerr.New("user.not_found", goerr.Kind(KindNotFound))
    ErrInternal     = goerr.New("internal", goerr.Kind(KindInternal))
)

func main() {
    // Repository returns a domain error
    err := ErrUserNotFound

    // Service adds context with fields
    err = goerr.Wrap(err, "failed to get user",
        goerr.Field("user_id", 123),
        goerr.Field("action", "login"))

    // Handler adds more context
    err = goerr.Wrap(err, "GET /api/users/:id failed",
        goerr.Field("endpoint", "/api/users/123"),
        goerr.Field("method", "GET"))

    // Client sees only the primary business error
    fmt.Println("Error:", err.Error())
    // Output: user.not_found

    // Logs show full context with fields
    fmt.Printf("Details: %v\n", err)
    // Output: user.not_found (kind=not_found): GET /api/users/:id failed (endpoint=/api/users/123, method=GET): failed to get user (action=login, user_id=123)

    // Access error kind for transport mapping
    goErr := goerr.FromError(err)
    fmt.Println("Kind:", goErr.Kind())
    // Output: not_found

    // Access specific fields
    if userID, ok := goErr.GetField("user_id"); ok {
        fmt.Println("User ID:", userID)
        // Output: User ID: 123
    }
}

Usage Patterns

Creating Domain Errors
// Define error kinds
var (
    KindNotFound = errors.New("not_found")
    KindInvalid  = errors.New("invalid")
    KindInternal = errors.New("internal")
)

// Define domain-specific errors with kinds
var (
    ErrUserNotFound  = goerr.New("user.not_found", goerr.Kind(KindNotFound))
    ErrUserBanned    = goerr.New("user.banned", goerr.Kind(KindInvalid))
    ErrOrderExpired  = goerr.New("order.expired", goerr.Kind(KindInvalid))
    ErrDatabaseError = goerr.New("internal", goerr.Kind(KindInternal))
)
Wrapping Errors with Context
// Repository layer
func (r *Repository) GetUser(ctx context.Context, id string) (*User, error) {
    user, err := r.db.Find(id)
    if err != nil {
        if errors.Is(err, gorm.ErrRecordNotFound) {
            return nil, ErrUserNotFound
        }
        return nil, ErrDatabaseError
    }
    return user, nil
}

// Service layer
func (s *Service) GetUser(ctx context.Context, id string) (*User, error) {
    user, err := s.repo.GetUser(ctx, id)
    if err != nil {
        return nil, goerr.Wrap(err, "failed to get user",
            goerr.Field("user_id", id))
    }
    return user, nil
}

// Handler layer
func (h *Handler) GetUser(w http.ResponseWriter, r *http.Request) {
    id := r.PathValue("id")
    user, err := h.service.GetUser(r.Context(), id)
    if err != nil {
        log.Error("request failed",
            "error", err, // Full context in logs
            "request_id", middleware.GetReqID(r.Context()))

        // Map error kind to HTTP status
        statusCode := MapErrorToHTTP(err)
        http.Error(w, err.Error(), statusCode)
        return
    }
    // ...
}
Mapping Error Kinds to Transport Codes
// HTTP mapping
func MapErrorToHTTP(err error) int {
    goErr := goerr.FromError(err)
    switch goErr.Kind() {
    case KindNotFound:
        return http.StatusNotFound
    case KindInvalid:
        return http.StatusBadRequest
    case KindInternal:
        return http.StatusInternalServerError
    default:
        return http.StatusInternalServerError
    }
}

// gRPC mapping
func MapErrorToGRPC(err error) codes.Code {
    goErr := goerr.FromError(err)
    switch goErr.Kind() {
    case KindNotFound:
        return codes.NotFound
    case KindInvalid:
        return codes.InvalidArgument
    case KindInternal:
        return codes.Internal
    default:
        return codes.Unknown
    }
}

API Reference

Creating Errors
goerr.New(main any, args ...any) error

Creates a new error with a primary message/error and optional wrapped errors or options.

err := goerr.New("user.not_found",
    goerr.Kind(KindNotFound),
    goerr.Field("user_id", 123))
goerr.Wrap(main any, context any, opts ...Option) error

Wraps an error with additional context and fields. Preserves the original error's kind.

err := goerr.Wrap(dbErr, "failed to query database",
    goerr.Field("query", "SELECT * FROM users"),
    goerr.Field("duration_ms", 150),
)
goerr.FromError(err error) *Error

Converts any error to *goerr.Error, preserving context if already a goerr error.

Options
goerr.Kind(kind error) Option

Sets the error kind for transport mapping.

goerr.Field(key string, value any) Option

Adds a key-value metadata field.

goerr.Fields(fields map[string]any) Option

Adds multiple metadata fields at once.

err := goerr.New("error",
    goerr.Fields(map[string]any{
        "user_id": 123,
        "action": "login",
        "ip": "192.168.1.1",
    }),
)
Methods
(*Error).Error() string

Returns the primary business error message (safe for client display).

(*Error).Details() string

Returns the full error chain with context and fields, intended for logs and diagnostics.

(*Error).Kind() error

Returns the error kind for transport mapping.

(*Error).GetField(key string) (any, bool)

Retrieves a field value from the current error level only.

(*Error).Fields() map[string]any

Returns a copy of fields attached to the current error level.

(*Error).Unwrap() error

Returns the next wrapped error in the chain.

(*Error).Format(s fmt.State, verb rune)

Custom formatter: %v for details, %s for primary error, %q for quoted.

Deprecated Methods

The following methods are deprecated and will be removed in a future version:

  • WithError(error) Option - use wrapping via New() or Wrap() instead
  • WithField(key, value) Option - use Field() instead
  • WithHTTPCode(int) Option - use Kind() with error kind mapping instead
  • (*Error).HTTPCode() int - use Kind() and transport-specific mapping instead

Why goerr?

Traditional Go error handling with fmt.Errorf and %w has limitations:

  • No structured metadata: Can't attach fields like user_id or request_id
  • Verbose error chains: User-facing messages are intentionally minimal.
  • No error classification: Hard to map errors to different transport codes (HTTP, gRPC)

Full context is available only through formatting (%v) or Details().

goerr solves these problems by:

  • Separating user-facing error codes from internal context
  • Providing structured metadata for rich logging
  • Classifying errors by kind for consistent transport mapping
  • Maintaining full compatibility with standard Go error handling

Contributing

Contributions are welcome! Please open an issue or pull request at github.com/tyrenix/goerr.

License

MIT — see LICENSE

Documentation

Index

Constants

This section is empty.

Variables

View Source
var (
	ErrForbidden    = New("forbidden", Kind(KindForbidden))
	ErrNotFound     = New("not_found", Kind(KindNotFound))
	ErrInvalid      = New("invalid", Kind(KindInvalid))
	ErrConflict     = New("conflict", Kind(KindConflict))
	ErrUnauthorized = New("unauthorized", Kind(KindUnauthorized))
	ErrInternal     = New("internal", Kind(KindInternal))
)

Built-in sentinel errors for cross-package errors.Is checks.

Functions

func New

func New(cause any, opts ...Option) error

New creates a new Error with a cause error and optional configurations.

func Wrap added in v1.5.0

func Wrap(prev error, context any, opts ...Option) error

Wrap wraps an error with a cause error and optional configurations.

Types

type Error

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

Error represents a custom error with a main error, wrapped errors, and fields.

func FromError added in v1.2.0

func FromError(err error) *Error

FromError converts any error to *Error, preserving context if possible.

func (*Error) Details added in v1.3.0

func (e *Error) Details() string

Details returns the error details as a string.

func (*Error) Error

func (e *Error) Error() string

Error returns the main error message.

func (*Error) Fields added in v1.2.0

func (e *Error) Fields() map[string]any

Fields returns the map of custom fields.

func (*Error) Format

func (e *Error) Format(s fmt.State, verb rune)

Format implements fmt.Formatter for custom formatting.

func (*Error) GetField added in v1.2.0

func (e *Error) GetField(key string) (any, bool)

GetField returns the value of a field by key, with a boolean indicating if it exists. It searches for the key recursively through the entire error chain.

func (*Error) GetFieldDeep added in v1.5.4

func (e *Error) GetFieldDeep(key string) (any, bool)

GetFieldDeep returns the value of a field by key, with a boolean indicating if it exists.

func (*Error) HTTPCode deprecated added in v1.1.0

func (e *Error) HTTPCode() int

Deprecated: HTTPCode is deprecated, this method is deprecated and will be removed in a future version.

func (*Error) Is added in v1.2.0

func (e *Error) Is(target error) bool

Is returns true if the target error is the same as the cause or wrapped error.

func (*Error) Kind added in v1.5.0

func (e *Error) Kind() KindValue

Kind returns the kind of the error.

func (*Error) Unwrap

func (e *Error) Unwrap() error

Unwrap returns all wrapped errors for compatibility with Go 1.20+ errors.Is/As.

type KindValue added in v1.6.0

type KindValue string

KindValue represents an error kind

const (
	KindForbidden    KindValue = "forbidden"
	KindNotFound     KindValue = "not_found"
	KindInvalid      KindValue = "invalid"
	KindConflict     KindValue = "conflict"
	KindUnauthorized KindValue = "unauthorized"
	KindInternal     KindValue = "internal"
)

Built-in error kinds.

type Option added in v1.1.0

type Option func(*Error)

Option defines a functional option for modifying the Error struct.

func Field added in v1.5.0

func Field(key string, value any) Option

Field adds a key-value pair to the Error's fields.

func Fields added in v1.5.0

func Fields(fields map[string]any) Option

Fields adds multiple key-value pairs to the Error's fields.

func Kind added in v1.5.0

func Kind(kind KindValue) Option

Kind sets the kind of the Error.

func Op added in v1.5.5

func Op(op string) Option

Op adds a key-value pair to the Error's fields. A shortcut for Field("op", op).

func WithError deprecated added in v1.2.0

func WithError(wrapped error) Option

Deprecated: WithError is deprecated, this method is deprecated and will be removed in a future version.

func WithField deprecated added in v1.2.0

func WithField(key string, value any) Option

Deprecated: WithField is deprecated, use Field instead.

func WithHTTPCode deprecated added in v1.1.0

func WithHTTPCode(code int) Option

Deprecated: WithHTTPCode is deprecated, this method is deprecated and will be removed in a future version.

Directories

Path Synopsis

Jump to

Keyboard shortcuts

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