apperr

package module
v0.1.1 Latest Latest
Warning

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

Go to latest
Published: Mar 18, 2026 License: MIT Imports: 2 Imported by: 2

README

apperr

Transport-agnostic application error handling for Go
Define errors once, return them directly, and let the transport layer handle the mapping.

GitHub tag Go Reference Go Report Card License

Introduction

Why

A common pattern in Go backends — even those following Clean Architecture — is leaking transport concerns into business logic:

func (s *userService) Login(ctx context.Context, email, password string) (User, error) {
    user, err := s.repo.FindByEmail(ctx, email)
    if err != nil {
        // ❌ HTTP status in business logic (imports net/http)
        return User{}, echo.NewHTTPError(http.StatusNotFound, "user not found")
    }

    if user.IsBlocked {
        // ❌ No error code — frontend can't programmatically handle this
        // ❌ No metadata — frontend can't show "try again in 5 minutes"
        // ❌ Hardcoded message — can't do i18n
        return User{}, echo.NewHTTPError(http.StatusForbidden, "account blocked")
    }

    return user, nil
}

This couples your service layer to HTTP, makes errors impossible to translate (i18n), and gives the frontend no structured way to handle specific error cases.

How

With apperr, errors are defined once as Definitions — transport-agnostic, with a unique Code and Kind. Return them directly from any layer:

func (s *userService) Login(ctx context.Context, email, password string) (User, error) {
    user, err := s.repo.FindByEmail(ctx, email)
    if err != nil {
        return User{}, errcodes.ErrUserNotFound       // ✅ transport-agnostic, no net/http
    }

    if user.IsBlocked {
        return User{}, errcodes.ErrAccountBlocked.     // ✅ error code: "AUTH_ACCOUNT_BLOCKED"
            WithMeta("retry_after_minutes", 5)          // ✅ structured metadata for the frontend
    }

    return user, nil
}

The transport layer maps Kind to the appropriate status code automatically. Your business logic never imports net/http, and the frontend gets structured code + meta for i18n and programmatic handling.

Install

go get github.com/diegoclair/apperr

Getting Started

1. Define your errors (once)

Create a package in your project with all error definitions. Each Definition has a Kind (category), a Code (unique identifier for the frontend), and a default message:

package errcodes

import "github.com/diegoclair/apperr"

var (
    ErrUserNotFound   = apperr.Define(apperr.KindNotFound, "USER_NOT_FOUND", "user not found")
    ErrEmailExists    = apperr.Define(apperr.KindConflict, "USER_EMAIL_EXISTS", "email already exists")
    ErrLoginFailed    = apperr.Define(apperr.KindAuthentication, "AUTH_LOGIN_FAILED", "invalid credentials")
    ErrAccountBlocked = apperr.Define(apperr.KindAuthentication, "AUTH_ACCOUNT_BLOCKED", "account blocked")
)

The library also provides built-in sentinel errors for common cases like ErrNotFound, ErrInternal, ErrTokenExpired, etc.

2. Return errors from your services
// Simple — return the Definition directly (it implements error)
return errcodes.ErrUserNotFound

// With metadata — creates a new *Error instance (immutable)
return errcodes.ErrLoginFailed.WithMeta("remaining_attempts", 2)

// With cause — preserves the original error for logging
return errcodes.ErrUserNotFound.Wrap(err)

// With custom message — overrides the default
return errcodes.ErrEmailExists.WithMessage("the email john@example.com is already in use")
3. Check errors

All helpers use errors.As under the hood, so they work with wrapped errors (e.g., fmt.Errorf("repo: %w", err)):

// By Kind (category)
if apperr.IsNotFound(err) { ... }
if apperr.IsValidation(err) { ... }
if apperr.IsAuthentication(err) { ... }

// By specific error (compares Code via errors.Is)
if errors.Is(err, errcodes.ErrAccountBlocked) { ... }

// Extract code
code := apperr.GetCode(err) // "AUTH_ACCOUNT_BLOCKED"
4. Map to HTTP (in your transport layer)

The httpmap sub-package converts any AppError into an HTTP response. Import it only in your transport layer — the core apperr package has zero dependency on net/http:

import "github.com/diegoclair/apperr/httpmap"

func handleError(w http.ResponseWriter, err error) {
    status, response := httpmap.ToHTTP(err)
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(status)
    json.NewEncoder(w).Encode(response)
}

JSON response:

{
    "message": "invalid credentials",
    "status_code": 401,
    "error": "Unauthorized",
    "code": "AUTH_LOGIN_FAILED",
    "meta": {
        "remaining_attempts": 2
    }
}

The frontend uses code for i18n translations and programmatic handling, while meta carries dynamic data. The message field serves as a fallback for logging and debugging.

Architecture

apperr (core — zero net/http dependency)
├── AppError    interface (common between Definition and Error)
├── Definition  pre-defined error, returned directly as error
├── Error       error with context (cause, meta)
├── Kind        error category (Validation, NotFound, Conflict, ...)
├── Code        unique error identifier (string)
├── Helpers     IsNotFound(), IsValidation(), HasCode(), GetMeta(), ...
└── Sentinels   ErrNotFound, ErrInternal, ErrConflict, ... (generic, reusable)

httpmap/ (sub-package — maps Kind → HTTP status)
├── ToHTTP()        converts error → (statusCode, ErrorResponse)
├── StatusFromKind()
└── ErrorResponse   JSON-serializable response struct
Kind → HTTP Status Mapping
Kind HTTP Status
KindValidation 400 Bad Request
KindAuthentication 401 Unauthorized
KindAuthorization 403 Forbidden
KindNotFound 404 Not Found
KindConflict 409 Conflict
KindRateLimited 429 Too Many Requests
KindInternal 500 Internal Server Error
Built-in Sentinel Errors

Generic errors reusable in any project. For business-specific errors, define your own with Define().

Sentinel Kind Code
ErrValidation Validation VALIDATION_ERROR
ErrInvalidInput Validation INVALID_INPUT
ErrRequiredField Validation REQUIRED_FIELD
ErrInvalidFormat Validation INVALID_FORMAT
ErrNotFound NotFound NOT_FOUND
ErrRecordNotFound NotFound RECORD_NOT_FOUND
ErrConflict Conflict CONFLICT
ErrDuplicateEntry Conflict DUPLICATE_ENTRY
ErrUnauthenticated Authentication UNAUTHENTICATED
ErrTokenInvalid Authentication TOKEN_INVALID
ErrTokenExpired Authentication TOKEN_EXPIRED
ErrTokenRequired Authentication TOKEN_REQUIRED
ErrForbidden Authorization FORBIDDEN
ErrInternal Internal INTERNAL_ERROR
ErrRateLimited RateLimited RATE_LIMITED

Design Decisions

Why Definition + Error (two types)?
  • Definition is a static, pre-defined error — cheap to return, no allocations
  • Error adds context (cause, meta) — only created when needed via WithMeta(), Wrap(), or WithMessage()
  • Both implement AppError interface, so helpers and mappers work with either transparently
Why no global registry?

Definitions are package-level variables (var ErrXxx = apperr.Define(...)). This gives you:

  • IDE autocomplete (errcodes.Err → see all errors)
  • Compile-time safety (typos are caught)
  • Zero global state, zero init() functions
  • errors.Is() works via Code comparison
Why no New() method?

Definition implements error directly. You just return errcodes.ErrSomething. Only when you need to add context (meta, cause, custom message) do the methods create an *Error instance. Less verbosity, same safety.

Roadmap

  • grpcmap/ — Kind → gRPC status code mapping
  • gqlmap/ — Kind → GraphQL error extensions mapping

Contributing

Contributions are welcome!

  1. Fork the repository
  2. Create a feature branch (git checkout -b feature/my-feature)
  3. Commit your changes
  4. Push to the branch (git push origin feature/my-feature)
  5. Open a Pull Request

License

MIT

Documentation

Index

Constants

This section is empty.

Variables

View Source
var (
	// Validation
	ErrValidation    = Define(KindValidation, "VALIDATION_ERROR", "validation error")
	ErrInvalidInput  = Define(KindValidation, "INVALID_INPUT", "invalid input")
	ErrRequiredField = Define(KindValidation, "REQUIRED_FIELD", "required field is missing")
	ErrInvalidFormat = Define(KindValidation, "INVALID_FORMAT", "invalid format")

	// Not Found
	ErrNotFound       = Define(KindNotFound, "NOT_FOUND", "resource not found")
	ErrRecordNotFound = Define(KindNotFound, "RECORD_NOT_FOUND", "record not found")

	// Conflict
	ErrConflict       = Define(KindConflict, "CONFLICT", "resource already exists")
	ErrDuplicateEntry = Define(KindConflict, "DUPLICATE_ENTRY", "duplicate entry")

	// Authentication
	ErrUnauthenticated = Define(KindAuthentication, "UNAUTHENTICATED", "authentication required")
	ErrTokenInvalid    = Define(KindAuthentication, "TOKEN_INVALID", "token is invalid")
	ErrTokenExpired    = Define(KindAuthentication, "TOKEN_EXPIRED", "token has expired")
	ErrTokenRequired   = Define(KindAuthentication, "TOKEN_REQUIRED", "token is required")

	// Authorization
	ErrForbidden = Define(KindAuthorization, "FORBIDDEN", "access denied")

	// Internal
	ErrInternal = Define(KindInternal, "INTERNAL_ERROR", "internal server error")

	// Rate Limit
	ErrRateLimited = Define(KindRateLimited, "RATE_LIMITED", "rate limit exceeded")
)

Generic sentinel errors — common errors reusable in any project. For business-specific errors, define in your project with Define().

Functions

func GetMeta

func GetMeta(err error) map[string]any

GetMeta extracts the meta from an error chain. Returns nil if no *Error is found. Only *Error carries meta — *Definition and plain errors return nil.

func HasCode

func HasCode(err error, code Code) bool

HasCode checks if the error (or any error in its chain) has a specific Code.

func IsAuthentication

func IsAuthentication(err error) bool

func IsAuthorization

func IsAuthorization(err error) bool

func IsConflict

func IsConflict(err error) bool

func IsInternal

func IsInternal(err error) bool

func IsKind

func IsKind(err error, kind Kind) bool

IsKind checks if an error (or any error in its chain) is an AppError with the specified Kind. Uses errors.As under the hood, so it works with wrapped errors (e.g., fmt.Errorf("...: %w", err)).

func IsNotFound

func IsNotFound(err error) bool

func IsRateLimited

func IsRateLimited(err error) bool

func IsValidation

func IsValidation(err error) bool

Types

type AppError

type AppError interface {
	error
	Kind() Kind
	Code() Code
	Message() string
}

AppError is the common interface between Definition (static error) and Error (error with context). Both can be returned as error. Helpers like IsNotFound(), AsAppError(), and the httpmap package check for this interface.

func AsAppError

func AsAppError(err error) (AppError, bool)

AsAppError extracts the AppError from an error chain. Works with both *Definition and *Error, and traverses wrapped errors.

type Code

type Code string

Code is a unique and stable identifier for each system error. The frontend uses this code to translate messages (i18n). Convention: "DOMAIN_ACTION_REASON" (e.g., AUTH_LOGIN_BLOCKED, USER_EMAIL_EXISTS)

func GetCode

func GetCode(err error) Code

GetCode extracts the Code from an error (or any error in its chain). Returns "" if no AppError is found.

type Definition

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

Definition is a pre-defined (static) error. Implements AppError and error — can be returned directly as error. Each project defines its Definitions as package-level variables.

Simple usage (direct return):

return errcodes.ErrAccountBlocked

With context (creates *Error):

return errcodes.ErrLoginFailed.WithMeta("remaining_attempts", 2)
return errcodes.ErrInternal.Wrap(err)

func Define

func Define(kind Kind, code Code, message string) *Definition

Define creates an error Definition. Called once at package initialization.

Example:

var ErrAccountBlocked = apperr.Define(apperr.KindAuthentication, "AUTH_ACCOUNT_BLOCKED", "account blocked due to failed login attempts")

func (*Definition) Code

func (d *Definition) Code() Code

func (*Definition) Error

func (d *Definition) Error() string

Error implements the error interface — allows direct return as error.

func (*Definition) Is

func (d *Definition) Is(target error) bool

Is allows comparison with errors.Is() — compares by Code. Works with both *Error and *Definition as target.

func (*Definition) Kind

func (d *Definition) Kind() Kind

func (*Definition) Message

func (d *Definition) Message() string

func (*Definition) WithMessage

func (d *Definition) WithMessage(message string) *Error

WithMessage creates an *Error with a custom message (overrides the default). Useful when the message needs dynamic context.

func (*Definition) WithMeta

func (d *Definition) WithMeta(key string, value any) *Error

WithMeta creates an *Error with metadata. Used for dynamic data that the frontend needs (e.g., remaining_attempts, field_name).

func (*Definition) Wrap

func (d *Definition) Wrap(cause error) *Error

Wrap creates an *Error from this Definition, wrapping an original error. Use when you want to preserve the original error for logging.

type Error

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

Error is a domain error with context (cause, meta). Created when extra information needs to be added to a Definition, via WithMeta, Wrap, or WithMessage. Implements AppError and Go's error interface.

func (*Error) Code

func (e *Error) Code() Code

func (*Error) Error

func (e *Error) Error() string

func (*Error) Is

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

Is allows comparison with errors.Is() — compares by Code. Works with both *Error and *Definition as target.

func (*Error) Kind

func (e *Error) Kind() Kind

func (*Error) Message

func (e *Error) Message() string

func (*Error) Meta

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

func (*Error) Unwrap

func (e *Error) Unwrap() error

func (*Error) WithCause

func (e *Error) WithCause(cause error) *Error

WithCause returns a new Error with the cause added (immutable).

func (*Error) WithMeta

func (e *Error) WithMeta(key string, value any) *Error

WithMeta returns a new Error with the metadata added (immutable).

type Kind

type Kind uint8

Kind represents the category of an application error. It is transport-agnostic — the transport layer maps it to HTTP/gRPC/etc.

const (
	KindValidation     Kind = iota + 1 // input or business rule violation
	KindNotFound                       // resource not found
	KindConflict                       // duplicate, constraint violation
	KindAuthentication                 // invalid credentials, expired token
	KindAuthorization                  // no permission for the resource
	KindInternal                       // unexpected system error
	KindRateLimited                    // rate limit exceeded
)

func (Kind) String

func (k Kind) String() string

String returns the name of the Kind (useful for logs and debug).

Directories

Path Synopsis
Package httpmap provides Kind → HTTP status code mapping for REST APIs.
Package httpmap provides Kind → HTTP status code mapping for REST APIs.

Jump to

Keyboard shortcuts

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