xerrors

package module
v0.10.0 Latest Latest
Warning

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

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

README

xerrors

Structured application errors with stable codes, cause chaining, and zero-dependency log enrichment.

Module: code.nochebuena.dev/go/xerrors Tier: 0 — zero external dependencies, stdlib only Go: 1.25+ Dependencies: none


Overview

xerrors provides a single error type — Err — that carries a machine-readable Code, a human-readable message, an optional cause, and optional key-value context fields.

The Code values are stable string constants aligned with gRPC status codes. They are safe to persist, transmit in API responses, and switch on programmatically. The httputil module uses them to map errors to HTTP status codes automatically.

This package does not handle HTTP responses, logging, or i18n. Those concerns belong to httputil and logz respectively.

Installation

go get code.nochebuena.dev/go/xerrors

Quick start

import "code.nochebuena.dev/go/xerrors"

// Create a structured error
err := xerrors.New(xerrors.ErrNotFound, "user not found")

// With cause chain
err := xerrors.Wrap(xerrors.ErrInternal, "failed to query database", dbErr)

// Convenience constructors (fmt.Sprintf-style)
err := xerrors.NotFound("user %s not found", userID)

// Builder pattern — attach structured context for logging
err := xerrors.New(xerrors.ErrInvalidInput, "validation failed").
    WithContext("field", "email").
    WithContext("rule", "required")

// Walk the cause chain with stdlib
var e *xerrors.Err
if errors.As(err, &e) {
    fmt.Println(e.Code())    // ErrInvalidInput
    fmt.Println(e.Message()) // "validation failed"
}

Usage

Creating errors
Function Code Use when
New(code, message) any general purpose
Wrap(code, message, err) any wrapping a lower-level error
InvalidInput(msg, args...) ErrInvalidInput bad or missing request data
NotFound(msg, args...) ErrNotFound resource does not exist
Internal(msg, args...) ErrInternal unexpected server-side failure
Attaching context fields

Context fields are key-value pairs that enrich log records and debug output. They never appear in API responses.

err := xerrors.New(xerrors.ErrInvalidInput, "validation failed").
    WithContext("field", "email").
    WithContext("rule", "required").
    WithContext("value", input.Email)

WithContext can be chained and called multiple times. Repeating a key overwrites the previous value.

Cause chaining

Wrap and WithError both set the underlying cause. Err.Unwrap is implemented, so errors.Is and errors.As walk the full chain:

err := xerrors.Wrap(xerrors.ErrInternal, "save failed", io.ErrUnexpectedEOF)

errors.Is(err, io.ErrUnexpectedEOF) // true

var e *xerrors.Err
errors.As(err, &e) // true — works through fmt.Errorf("%w", ...) wrapping too
Reading errors
var e *xerrors.Err
if errors.As(err, &e) {
    e.Code()     // xerrors.Code — the typed error category
    e.Message()  // string — the human-readable message
    e.Fields()   // map[string]any — shallow copy of context fields
    e.Unwrap()   // error — the underlying cause
    e.Detailed() // string — verbose debug string: "code: X | message: Y | cause: Z | fields: {...}"
}

Fields() always returns a non-nil map and is safe to mutate — it is a shallow copy of the internal state.

JSON serialization

Err implements json.Marshaler. This is what httputil uses to write error responses:

{
  "code": "NOT_FOUND",
  "message": "user abc123 not found",
  "fields": {
    "id": "abc123"
  }
}

fields is omitted when empty.

Structured log enrichment (duck-typing bridge)

logz automatically enriches log records when it receives an *Err — no import of xerrors needed by logz, and no import of logz needed here. The bridge works through two methods that Err exposes:

// Called by logz internally via errors.As — never call these directly.
func (e *Err) ErrorCode() string           // → "NOT_FOUND"
func (e *Err) ErrorContext() map[string]any // → the raw fields map

Passing an *Err to logger.Error(msg, err) automatically adds error_code and all context fields to the log record.

Codes

Wire values are gRPC status code names. HTTP mapping is the transport layer's responsibility.

Constant Wire value HTTP status
ErrInvalidInput INVALID_ARGUMENT 400
ErrUnauthorized UNAUTHENTICATED 401
ErrPermissionDenied PERMISSION_DENIED 403
ErrNotFound NOT_FOUND 404
ErrAlreadyExists ALREADY_EXISTS 409
ErrGone GONE 410
ErrPreconditionFailed FAILED_PRECONDITION 412
ErrRateLimited RESOURCE_EXHAUSTED 429
ErrCancelled CANCELLED 499
ErrInternal INTERNAL 500
ErrNotImplemented UNIMPLEMENTED 501
ErrUnavailable UNAVAILABLE 503
ErrDeadlineExceeded DEADLINE_EXCEEDED 504

Wire values are stable across versions — do not change them. Adding new constants is non-breaking.

Code.Description() returns a short human-readable description of any code.

Design decisions

Err instead of AppErr — the "App" prefix is redundant inside a package already named xerrors. xerrors.Err reads cleanly at call sites.

Code instead of ErrorCode — same reasoning. xerrors.Code is more concise.

Fields() returns a defensive copy — the internal map is not exposed directly. Callers who want read-only access to the raw map (e.g. logz) use ErrorContext(). Callers who need to manipulate the result use Fields().

EnsureAppError dropped — auto-wrapping arbitrary errors into a structured error hides the real cause and discourages explicit error handling. Use errors.As to check for *Err and handle each case intentionally.

Wire values aligned with gRPC — switching to gRPC (or adding gRPC alongside HTTP) requires no translation layer for most codes.

Ecosystem

Tier 0:   xerrors ← you are here
               ↑
Tier 1:   logz (duck-types xerrors — no direct import)
          valid (depends on xerrors for error construction)
               ↑
Tier 2:   httputil (maps xerrors.Code → HTTP status)
               ↑
Tier 4:   httpmw, httpauth, httpserver

Modules that consume xerrors errors without importing this package: logz (via ErrorCode() / ErrorContext() duck-typing).

License

MIT

Documentation

Overview

Package xerrors provides structured application errors with stable machine-readable codes, human-readable messages, cause chaining, and key-value context fields.

Each error carries a Code that maps to a well-known category (invalid input, not found, internal, etc.) and is stable across versions — safe to persist, transmit in API responses, or switch on programmatically.

Basic usage

err := xerrors.New(xerrors.ErrNotFound, "user not found")

// With cause chaining
err := xerrors.Wrap(xerrors.ErrInternal, "failed to query database", dbErr)

// Convenience constructors
err := xerrors.NotFound("user %s not found", userID)

// Builder pattern
err := xerrors.New(xerrors.ErrInvalidInput, "validation failed").
    WithContext("field", "email").
    WithContext("tag", "required")

Cause chaining

Err.Unwrap is implemented, so errors.Is and errors.As walk the full cause chain:

if errors.Is(err, io.ErrUnexpectedEOF) { ... }

var e *xerrors.Err
if errors.As(err, &e) {
    log.Println(e.Code())
}

Structured logging (duck-typing bridge)

Err.ErrorCode and Err.ErrorContext satisfy the private interfaces that logz defines internally. Passing an *Err to logger.Error automatically enriches the log record with error_code and context fields — without xerrors importing logz or logz importing xerrors.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Code

type Code string

Code is the machine-readable error category. Wire values are stable across versions and are identical to gRPC status code names. HTTP mapping is the responsibility of the transport layer, not this package.

const (
	// ErrInvalidInput indicates the request contains malformed or invalid data.
	// The caller should fix the input before retrying.
	ErrInvalidInput Code = "INVALID_ARGUMENT"

	// ErrUnauthorized indicates the request lacks valid authentication credentials.
	// The caller should authenticate and retry.
	ErrUnauthorized Code = "UNAUTHENTICATED"

	// ErrPermissionDenied indicates the authenticated caller lacks permission for the operation.
	// Authentication is not the issue — the caller is authenticated but not authorised.
	ErrPermissionDenied Code = "PERMISSION_DENIED"

	// ErrNotFound indicates the requested resource does not exist.
	ErrNotFound Code = "NOT_FOUND"

	// ErrAlreadyExists indicates a resource with the same identifier already exists.
	// Use for creation conflicts (e.g. duplicate email on sign-up).
	// For state-based conflicts not related to creation, use ErrPreconditionFailed.
	ErrAlreadyExists Code = "ALREADY_EXISTS"

	// ErrGone indicates the resource existed but has been permanently removed.
	// Unlike ErrNotFound, this signals the caller should not retry — the resource
	// is gone for good (e.g. a soft-deleted record that has been purged).
	ErrGone Code = "GONE"

	// ErrPreconditionFailed indicates the operation was rejected because a required
	// condition was not met. The input is valid but a business rule blocks the action
	// (e.g. "cannot delete an account with active subscriptions", or an optimistic-lock
	// mismatch). Different from ErrAlreadyExists (duplicate creation) and
	// ErrInvalidInput (bad data).
	ErrPreconditionFailed Code = "FAILED_PRECONDITION"

	// ErrRateLimited indicates the caller has exceeded a rate limit or exhausted a quota.
	ErrRateLimited Code = "RESOURCE_EXHAUSTED"

	// ErrCancelled indicates the operation was cancelled, typically because the caller
	// disconnected or the request context was cancelled.
	// Useful for translating context.Canceled to a structured error at service boundaries.
	ErrCancelled Code = "CANCELLED"

	// ErrInternal indicates an unexpected server-side failure.
	// This code should not be used when a more specific code applies.
	ErrInternal Code = "INTERNAL"

	// ErrNotImplemented indicates the requested operation has not been implemented.
	ErrNotImplemented Code = "UNIMPLEMENTED"

	// ErrUnavailable indicates the service is temporarily unable to handle requests.
	// The caller may retry with backoff.
	ErrUnavailable Code = "UNAVAILABLE"

	// ErrDeadlineExceeded indicates the operation timed out before completing.
	ErrDeadlineExceeded Code = "DEADLINE_EXCEEDED"
)

func (Code) Description

func (c Code) Description() string

Description returns a human-readable description for the code. Unknown codes return their raw string value.

type Err

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

Err is a structured application error carrying a Code, a human-readable message, an optional cause, and optional key-value context fields.

It implements the standard error interface, errors.Unwrap for cause chaining, and json.Marshaler for API responses. It also satisfies the private duck-typing interfaces that logz uses internally to enrich log records — without either package importing the other.

Use the builder methods Err.WithContext, Err.WithError, and Err.WithPlatformCode to attach additional information after construction:

err := xerrors.New(xerrors.ErrInvalidInput, "validation failed").
    WithContext("field", "email").
    WithContext("rule", "required").
    WithError(cause)

err := xerrors.New(xerrors.ErrNotFound, "employee not found").
    WithPlatformCode("EMPLOYEE_NOT_FOUND")

func Internal

func Internal(msg string, args ...any) *Err

Internal creates an Err with ErrInternal code. msg is formatted with args using fmt.Sprintf rules.

func InvalidInput

func InvalidInput(msg string, args ...any) *Err

InvalidInput creates an Err with ErrInvalidInput code. msg is formatted with args using fmt.Sprintf rules.

func New

func New(code Code, message string) *Err

New creates an Err with the given code and message. No cause is set.

func NotFound

func NotFound(msg string, args ...any) *Err

NotFound creates an Err with ErrNotFound code. msg is formatted with args using fmt.Sprintf rules.

func Wrap

func Wrap(code Code, message string, err error) *Err

Wrap creates an Err that wraps an existing error with a code and message. The wrapped error is accessible via errors.Is, errors.As, and Err.Unwrap.

func (*Err) Code

func (e *Err) Code() Code

Code returns the typed error code.

func (*Err) Detailed

func (e *Err) Detailed() string

Detailed returns a verbose string useful for debugging. Format: "code: X | message: Y | cause: Z | fields: {...}"

func (*Err) Error

func (e *Err) Error() string

Error implements the error interface. Format: "INVALID_ARGUMENT: username is required → original cause"

func (*Err) ErrorCode

func (e *Err) ErrorCode() string

ErrorCode returns the string value of the error code.

This method satisfies the private errorWithCode interface that logz defines internally. Passing an *Err to logger.Error automatically enriches the log record with an error_code field — without xerrors importing logz.

func (*Err) ErrorContext

func (e *Err) ErrorContext() map[string]any

ErrorContext returns the raw context fields map.

This method satisfies the private errorWithContext interface that logz defines internally. The returned map is used read-only by logz; callers who need a safe copy should use Err.Fields instead.

func (*Err) Fields

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

Fields returns a shallow copy of the context fields. Returns an empty (non-nil) map if no fields have been set.

func (*Err) MarshalJSON

func (e *Err) MarshalJSON() ([]byte, error)

MarshalJSON implements json.Marshaler. Output: {"code":"NOT_FOUND","platformCode":"EMPLOYEE_NOT_FOUND","message":"...","fields":{...}} platformCode and fields are omitted when empty.

func (*Err) Message

func (e *Err) Message() string

Message returns the human-readable error message.

func (*Err) PlatformCode added in v0.10.0

func (e *Err) PlatformCode() string

PlatformCode returns the platform-level error code, or an empty string if none was set.

func (*Err) Unwrap

func (e *Err) Unwrap() error

Unwrap returns the underlying cause, enabling errors.Is and errors.As to walk the full cause chain.

func (*Err) WithContext

func (e *Err) WithContext(key string, value any) *Err

WithContext adds a key-value pair to the error's context fields and returns the receiver for chaining. Calling it multiple times with the same key overwrites the previous value.

func (*Err) WithError

func (e *Err) WithError(err error) *Err

WithError sets the underlying cause and returns the receiver for chaining.

func (*Err) WithPlatformCode added in v0.10.0

func (e *Err) WithPlatformCode(code string) *Err

WithPlatformCode sets a platform-level error code and returns the receiver for chaining. Platform codes are domain-specific identifiers (e.g. "EMPLOYEE_NOT_FOUND") that operate independently of the transport-level Code. They are intended for consuming applications — such as a frontend — that need to map errors to localised user-facing messages without relying on the generic transport code.

Platform codes are optional. Errors that do not have a user-actionable meaning (e.g. 500 internal errors, infrastructure failures) should not carry one; the consuming application renders a generic fallback in those cases.

Jump to

Keyboard shortcuts

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