failure

package module
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Jun 20, 2026 License: Apache-2.0 Imports: 7 Imported by: 5

README

failure

pipeline status coverage report

failure provides typed, HTTP-aware errors with optional metadata (code, description, payload) and helpers for attaching the name of the operation that produced an error — all built on top of the standard errors package, so errors.Is, errors.As and %w keep working.

Install

go get git.appkode.ru/pub/go/failure

To import this private module, create an access token in the Gitlab User Settings page and write it to ~/.netrc:

machine git.appkode.ru
  login oauth2
  password your-access-token

You also need to remove the section with url.insteadOf=https://git.appkode.ru/ from .gitconfig (if any). Background:

Typed errors

Every typed error is an Error[K] of a kind K. Build one with the generic New, or with the equivalent New<Kind>Error helper; Is / As report and extract the category anywhere in the chain:

err := failure.New[failure.NotFound]("user not found")

failure.Is[failure.NotFound](err) // true
err.Error()                       // "not found: user not found"

// The named helpers are thin wrappers over the generic form:
failure.NewNotFoundError("user not found") // == failure.New[failure.NotFound](...)
failure.IsNotFoundError(err)               // == failure.Is[failure.NotFound](err)

// As returns the concrete typed error:
if e, ok := failure.As[failure.NotFound](err); ok {
    _ = e.HTTPStatus() // 404
}

Available kinds and the status HTTPStatus maps them to:

Kind Helper HTTPStatus
InvalidArgument NewInvalidArgumentError 400 Bad Request
Unauthorized NewUnauthorizedError 401 Unauthorized
Forbidden NewForbiddenError 403 Forbidden
NotFound NewNotFoundError 404 Not Found
Timeout NewTimeoutError 408 Request Timeout
Conflict NewConflictError 409 Conflict
UnprocessableEntity NewUnprocessableEntityError 422 Unprocessable Entity
InternalServer / any other NewInternalServerError 500 Internal Server Error
failure.HTTPStatus(failure.New[failure.Forbidden]("nope")) // 403
failure.HTTPStatus(errors.New("boom"))                     // 500

The set of kinds is closed: Kind's methods are unexported, so external packages cannot define new categories.

Metadata: code, description, payload

Constructors accept options:

err := failure.NewInvalidArgumentError(
    "validation failed",
    failure.WithCode(failure.NewErrorCode("E_VALIDATION")),
    failure.WithDescription("email is required"),
    failure.WithPayload(map[string]string{"field": "email"}),
)

failure.Code(err)                                  // "E_VALIDATION"
failure.HasCode(err, failure.NewErrorCode("E_VALIDATION")) // true
failure.Description(err)                            // "email is required"
failure.Payload(err)                               // map[field:email]

Code, Description and Payload traverse the chain, so they keep working after the error is wrapped further up the stack.

Attaching a cause

WithCause attaches an underlying error. It stays reachable through errors.Is, errors.As and Cause, and is appended to the message. A nil cause is a no-op:

err := failure.NewNotFoundError("user not found", failure.WithCause(sql.ErrNoRows))

err.Error()                   // "not found: user not found: sql: no rows in result set"
errors.Is(err, sql.ErrNoRows) // true
failure.IsNotFoundError(err)  // true
failure.Cause(err)            // sql.ErrNoRows

Cause returns a single root by following the first branch of a multi-error. To inspect every branch of an errors.Join / MultiWrap node, use Causes, which returns the root cause of each branch:

failure.Causes(breadcrumb.MultiWrap(errA, errB)) // []error{rootA, rootB}

Stack traces

WithStack records the call stack at the point the error is created — an automatic alternative to threading Breadcrumb names through every function. The stack is captured once, survives wrapping, and is read back with StackTrace. It never appears in Error(), so logs stay terse unless you ask for it:

err := failure.NewNotFoundError("user 42", failure.WithStack())

for _, f := range failure.StackTrace(err) {
    fmt.Printf("%s\n\t%s:%d\n", f.Function, f.File, f.Line)
}
// myapp/repo.(*Users).Find    repo.go:17
// myapp/service.(*Users).Get  service.go:21
// myapp/api.getUser           api.go:25
// ...

The trace starts at the caller that built the error; the library's own frames are trimmed. StackTrace returns nil when no error in the chain captured a stack.

Breadcrumbs

Breadcrumb wraps errors with the name of the function that returned them, building a readable call trail without losing the underlying chain. The name nods to the trail Hansel and Gretel dropped to find their way home — see breadcrumb navigation:

func GetUser(id string) error {
    const breadcrumb failure.Breadcrumb = "GetUser"

    if err := repo.Find(id); err != nil {
        return breadcrumb.Wrap(err) // "GetUser: <err>"; nil stays nil
    }

    return nil
}

MultiWrap joins several errors under one breadcrumb on a single line (; -separated, so the message stays readable inside one-line log fields like a JSON message — unlike errors.Join, which separates with newlines). nil and all-nil inputs collapse to a nil error, and every branch stays reachable through errors.Is / errors.As / Causes:

return breadcrumb.MultiWrap(validateName(), validateEmail(), validateAge())

Development

make format   # gofumpt + gci
make lint     # golangci-lint
make test     # race tests + coverage report

License

Apache-2.0

Documentation

Overview

Package failure provides typed, HTTP-aware errors with optional code, description and payload metadata, plus helpers for wrapping errors with the name of the function that produced them.

Typed errors

Every typed error is an Error of a Kind (invalid argument, unauthorized, forbidden, not found, timeout, conflict, unprocessable entity, internal server). Build one with the generic New, or with the equivalent New<Kind>Error helper; Is / As report and extract the category anywhere in the chain, and HTTPStatus maps it to a status code:

err := failure.New[failure.NotFound]("user", failure.WithCode(failure.NewErrorCode("USER_404")))
failure.Is[failure.NotFound](err) // true
failure.HTTPStatus(err)           // 404
failure.Code(err)                 // "USER_404"

// The named helpers are thin wrappers over the generic form:
failure.NewNotFoundError("user") // == failure.New[failure.NotFound]("user")
failure.IsNotFoundError(err)     // == failure.Is[failure.NotFound](err)

// As returns the concrete typed error for direct inspection:
if e, ok := failure.As[failure.NotFound](err); ok {
	_ = e.Code()
}

WithCause attaches an underlying error that stays reachable through errors.Is, errors.As and Cause:

err := failure.NewNotFoundError("user not found", failure.WithCause(sql.ErrNoRows))
errors.Is(err, sql.ErrNoRows) // true

WithStack records the call stack where the error is created — an automatic alternative to threading Breadcrumb names by hand. It is read back with StackTrace, survives wrapping, and never appears in the Error() string:

err := failure.NewNotFoundError("user", failure.WithStack())
frames := failure.StackTrace(err) // []runtime.Frame, caller-first

Metadata and rendering

An error carries up to four pieces of metadata, each with a distinct audience:

  • message — the short, primary error text passed to New.
  • code — a stable, machine-readable identifier (see ErrorCode) for programmatic handling; inspect it with Code and HasCode.
  • description — a human-readable, client-facing message safe to surface to the caller of an API; read it with Description.
  • payload — structured key/value context (e.g. the offending field); read it with Payload. It is cloned on the way in and out, so it stays immutable.

Description and Error target different audiences. Description returns only the public, client-facing text. Error returns the full, private rendering — message, code, description, payload and cause concatenated — meant for logs, not for API responses: returning err.Error() to a client can leak the underlying cause and payload. Build client responses from Description (with HTTPStatus and Code), and reserve Error for logging.

Breadcrumbs

Breadcrumb wraps errors with the name of the function that returned them, building a readable call trail without losing the original chain:

const breadcrumb failure.Breadcrumb = "GetUser"
return breadcrumb.Wrap(err)

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Cause

func Cause(err error) error

Cause returns the root error of an error tree. It follows errors.Unwrap for single-wrapped errors and, for multi-error nodes (those implementing Unwrap() []error, such as the result of Breadcrumb.MultiWrap or errors.Join), it descends into the first non-nil branch. It returns nil for a nil error.

func Causes

func Causes(err error) []error

Causes returns the root cause of every branch of the nearest multi-error node (those implementing Unwrap() []error, such as the result of Breadcrumb.MultiWrap or errors.Join) in err's chain. It descends single-wrapped errors to reach that node, then applies Cause to each non-nil branch — so where Cause keeps only the first branch, Causes keeps them all. For an error without a multi-error node it returns a one-element slice holding Cause(err); for a nil error it returns nil.

func Description

func Description(err error) string

func HTTPStatus

func HTTPStatus(err error) int

HTTPStatus returns the HTTP status of the first typed error in err's chain, or 500 if none is present.

func HasCode

func HasCode(err error, code ErrorCode) bool

func Is

func Is[K Kind](err error) bool

Is reports whether err is, or wraps, an Error of kind K.

func IsConflictError

func IsConflictError(err error) bool

func IsForbiddenError

func IsForbiddenError(err error) bool

func IsInternalServerError

func IsInternalServerError(err error) bool

func IsInvalidArgumentError

func IsInvalidArgumentError(err error) bool

func IsNotFoundError

func IsNotFoundError(err error) bool

func IsTimeoutError

func IsTimeoutError(err error) bool

func IsUnauthorizedError

func IsUnauthorizedError(err error) bool

func IsUnprocessableEntityError

func IsUnprocessableEntityError(err error) bool

func New

func New[K Kind](message string, opts ...Option) error

New builds an error of kind K.

func NewConflictError

func NewConflictError(message string, opts ...Option) error

func NewForbiddenError

func NewForbiddenError(message string, opts ...Option) error

func NewInternalServerError

func NewInternalServerError(message string, opts ...Option) error

func NewInvalidArgumentError

func NewInvalidArgumentError(message string, opts ...Option) error

func NewNotFoundError

func NewNotFoundError(message string, opts ...Option) error

func NewTimeoutError

func NewTimeoutError(message string, opts ...Option) error

func NewUnauthorizedError

func NewUnauthorizedError(message string, opts ...Option) error

func NewUnprocessableEntityError

func NewUnprocessableEntityError(message string, opts ...Option) error

func Payload

func Payload(err error) map[string]string

func StackTrace added in v0.2.0

func StackTrace(err error) []runtime.Frame

StackTrace returns the call stack captured by the first error in err's chain that was created with WithStack, or nil if none was.

Types

type Breadcrumb string

Breadcrumb is the name of a function that can return an error. Wrapping an error with it prepends that name, so a chain of Wrap calls leaves a trail of names leading back to where the failure started — like the breadcrumbs Hansel and Gretel dropped to find their way home (see https://en.wikipedia.org/wiki/Breadcrumb_navigation).

Example:

func ThisFunction(fn func() error) error {
	const breadcrumb failure.Breadcrumb = "ThisFunction"

	if err := fn(); err != nil {
		return breadcrumb.Wrap(err)
	}

	return nil
}
func (b Breadcrumb) MultiWrap(errs ...error) error

MultiWrap joins several errors under one breadcrumb. nil and all-nil inputs collapse to a nil error. Unlike errors.Join the joined message stays on a single line ("; "-separated), so aggregated failures remain readable inside one-line log fields such as a JSON message string. Every branch stays reachable through errors.Is, errors.As and Unwrap() []error, so Cause and Causes can walk them.

func (b Breadcrumb) Wrap(err error) error

Wrap prepends the breadcrumb to err, leaving the original chain reachable through errors.Is, errors.As and Unwrap. A nil error stays nil.

type Conflict

type Conflict struct{}

type ConflictError

type ConflictError = Error[Conflict]

type Error

type Error[K Kind] struct {
	// contains filtered or unexported fields
}

Error is a typed error of category K carrying an optional code, description, payload and cause. Construct it with New (or a New<Kind>Error helper) and inspect it with Is / As.

func As

func As[K Kind](err error) (Error[K], bool)

As finds the first Error of kind K in err's chain and returns it with true, or the zero Error and false.

func (Error) Code

func (e Error) Code() ErrorCode

func (Error) Description

func (e Error) Description() string

func (Error[K]) Error

func (e Error[K]) Error() string

Error prepends the category prefix to the shared metadata.

func (Error[K]) HTTPStatus

func (e Error[K]) HTTPStatus() int

HTTPStatus reports the HTTP status code associated with the error's kind.

func (Error) Payload

func (e Error) Payload() map[string]string

func (Error) StackTrace added in v0.2.0

func (e Error) StackTrace() []runtime.Frame

StackTrace returns the call stack captured if the error was built with WithStack, or nil. The trace starts at the caller that created the error.

func (Error) Unwrap

func (e Error) Unwrap() error

Unwrap returns the cause attached via WithCause, or nil.

type ErrorCode

type ErrorCode string

func Code

func Code(err error) ErrorCode

func NewErrorCode

func NewErrorCode(s string) ErrorCode

func (ErrorCode) String

func (e ErrorCode) String() string

type Forbidden

type Forbidden struct{}

type ForbiddenError

type ForbiddenError = Error[Forbidden]

type InternalServer

type InternalServer struct{}

type InternalServerError

type InternalServerError = Error[InternalServer]

type InvalidArgument

type InvalidArgument struct{}

type InvalidArgumentError

type InvalidArgumentError = Error[InvalidArgument]

type Kind

type Kind interface {
	// contains filtered or unexported methods
}

Kind identifies an error category (its human-readable prefix and HTTP status). Its methods are unexported on purpose: the set of kinds is closed, so external packages cannot define new ones.

type NotFound

type NotFound struct{}

type NotFoundError

type NotFoundError = Error[NotFound]

type Option

type Option func(*baseError)

func WithCause

func WithCause(cause error) Option

WithCause attaches an underlying cause to the error. The cause is reachable through errors.Is, errors.As and Cause, and is appended to the Error() string. A nil cause is a no-op.

func WithCode

func WithCode(code ErrorCode) Option

func WithDescription

func WithDescription(description string) Option

func WithPayload

func WithPayload(payload map[string]string) Option

func WithStack added in v0.2.0

func WithStack() Option

WithStack records the call stack at the point the error is created, so the failure can be traced without threading Breadcrumb names through every call. Read it back with StackTrace; it is omitted from the Error() string. The stack itself is captured by newBaseError once all options are applied.

type Timeout

type Timeout struct{}

type TimeoutError

type TimeoutError = Error[Timeout]

type Unauthorized

type Unauthorized struct{}

type UnauthorizedError

type UnauthorizedError = Error[Unauthorized]

type UnprocessableEntity

type UnprocessableEntity struct{}

type UnprocessableEntityError

type UnprocessableEntityError = Error[UnprocessableEntity]

Source Files

  • base_error.go
  • breadcrumb.go
  • cause.go
  • doc.go
  • error.go
  • error_code.go
  • failure.go
  • kind.go
  • option.go
  • stack.go

Jump to

Keyboard shortcuts

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