errors

package module
v1.1.0 Latest Latest
Warning

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

Go to latest
Published: Jul 10, 2026 License: Apache-2.0 Imports: 9 Imported by: 20

README

Errors Package

A structured error handling package for Go applications that provides enhanced error management with categories, codes, stack traces, and error chaining.

Installation

go get github.com/xhanio/errors

Features

  • Error Categories: Predefined HTTP-compatible error categories (BadRequest, NotFound, Internal, etc.)
  • Error Codes: Custom error codes with key-value details for structured error identification
  • Stack Traces: Automatic stack trace capture for debugging
  • Error Chaining: Support for wrapping and chaining errors with context
  • Flexible Formatting: Multiple output formats including structured details and stack traces
  • Error Traversal: Methods to navigate error chains and find specific causes

Integration with Standard Errors

Errors from this package implement Unwrap() error, so the standard library traverses them like any other wrapped error. Chains may mix freely with errors from other packages, in either direction:

// Standard error
stdErr := fmt.Errorf("standard error")

// Wrap with enhanced error
enhanced := errors.Wrapf(stdErr, "enhanced context")

// Wrap returns nil if the parent error is nil
err := errors.Wrap(nil)                   // err is nil
err := errors.Wrapf(nil, "some message")  // err is still nil

// stdlib errors.Is / errors.As / errors.Unwrap all walk our chains,
// including through wrappers this package did not create
stderrors.Is(enhanced, stdErr)  // true

Is and As are re-exported so you need not import errors under an alias alongside this package. One exception: a category is stored on an error rather than wrapped by it, so the standard library cannot see one. Use this package's Is for category checks.

Quick Start

Creating Simple Errors
// Simple error with message
err := errors.Newf("user not found")

// Category as error directly
err := errors.NotFound

// Using category shorthand
err := errors.NotFound.Newf("user %d not found", userID)
Wrapping Errors
// Direct wrap (preserves existing error, adds stack trace if not already an Error)
if err != nil {
    return errors.Wrap(err)
}

// Simple wrap with message
err := errors.Wrapf(originalErr, "processing failed for user %d", userID)

// Wrap with category
err := errors.Internal.Wrapf(dbErr, "database operation failed")
Always Give an Error a Message

Never write errors.Newf("") or errors.Wrapf(err, ""). Both compile, both look harmless, and both destroy information.

err := errors.Newf("")
err != nil      // true  -- the caller takes its error branch
err.Error()     // ""    -- and whatever logs it records nothing

An error with no message reports that something failed while withholding what. It also corrupts Combine, which joins messages with "; ":

errors.Combine(errors.Newf(""), errors.Newf("real")).Error()  // "; real"

Wrapf(err, "") is a different trap. Wrap(err) is a no-op on an error this package produced, returning it unchanged. Wrapf(err, "") always applies an option, so it appends a message-less layer instead:

inner := errors.Newf("inner")

errors.Wrap(inner) == inner        // true  -- returns the same error
errors.Wrapf(inner, "") == inner   // false -- a new, invisible chain node

Both print "inner", but the second lengthens the chain with a node no reader can see. When you have no context to add, call Wrap(err). When you have context, say it.

Checking Errors

Is performs one of two searches, chosen by the type of the target.

// Target is a Category: resolves the error's single category.
// The outermost category in the chain wins.
if errors.Is(err, errors.NotFound) {
    // Handle not found error
}

// Target is any other error: searches the whole chain, via errors.Is.
if errors.Is(err, originalErr) {
    // Handle case where originalErr is in the chain
}

As recovers a concrete error type so you can read its fields. It has nothing to do with categories.

var pe *net.ParseError
if errors.As(err, &pe) {
    fmt.Println(pe.Type)
}

// To read a category, resolve it rather than searching for it:
if e, ok := err.(errors.Error); ok {
    status := e.Category().StatusCode()
}

Error Categories

The package provides predefined categories that map to HTTP status codes:

Category HTTP Status Description
BadRequest 400 Invalid request parameters
Unauthorized 401 Authentication required
Forbidden 403 Permission denied
NotFound 404 Resource not found
Conflict 409 Resource conflict
TooManyRequests 429 Rate limit exceeded
Internal 500 Internal server error
NotImplemented 501 Feature not implemented
Unavailable 503 Service unavailable

Advanced Usage

Creating Errors with Codes
// Error with category and code
err := errors.BadRequest.New(
    errors.WithCode("INVALID_EMAIL", map[string]string{"field": "email"}),
    errors.WithMessage("invalid email: %s", email),
)

// Wrap with additional options
err := errors.Wrap(originalErr, errors.WithMessage("failed to process user"))
Error Inspection
// Create structured error
err := errors.BadRequest.New(
    errors.WithCode("VALIDATION_FAILED", map[string]string{
        "field": "email",
        "type": "format",
    }),
    errors.WithMessage("email validation failed: %s", email),
)

// Check error properties
code, details := err.(errors.Error).Code()
category := err.(errors.Error).Category()
message := err.(errors.Error).Message()

fmt.Printf("Code: %s, Status: %d\n", code, category.StatusCode())
Error Chaining and Traversal
// Create error chain
dbErr := errors.Internal.Newf("connection failed")
serviceErr := errors.Wrapf(dbErr, "user service error")
apiErr := errors.BadRequest.Wrapf(serviceErr, "API request failed")

// Navigate the chain
rootCause := apiErr.(errors.Error).RootCause()  // Gets dbErr
chain := apiErr.(errors.Error).Chain()          // Gets [apiErr, serviceErr, dbErr]

// Check for specific errors
if errors.Is(apiErr, dbErr) {
    // Handle database-related error
}
Combining Multiple Errors
var errs []error
errs = append(errs, errors.Newf("error 1"))
errs = append(errs, errors.Newf("error 2"))

combined := errors.Combine(errs...)

Formatting and Output

Stack Traces

Stack traces are captured automatically when creating a new error, and when wrapping an error that does not already carry one. The %v verb includes the stack trace; %s and %m do not.

err := errors.Newf("something went wrong")
fmt.Printf("%v", err)  // message, then the full stack trace
fmt.Printf("%s", err)  // message only

Note that fmt.Errorf renders a %w operand with %v, so wrapping one of these errors with fmt.Errorf embeds the stack trace in the resulting message. Prefer errors.Wrapf, which keeps the stack where it belongs.

Format Options
err := errors.Internal.New(
    errors.WithCode("DB_ERROR", map[string]string{"table": "users"}),
    errors.WithMessage("database operation failed"),
)

// Different format options
fmt.Printf("%s", err)   // "database operation failed" (whole chain, joined by ": ")
fmt.Printf("%m", err)   // "database operation failed" (outermost message only)
fmt.Printf("%v", err)   // "{DB_ERROR:table=users}database operation failed\n[stack trace]"

Reference

Error Interface

Errors implement the Error interface providing these methods:

type Error interface {
    Message() string             // Outermost error message, without causes
    Error() string               // All messages in the chain, joined by ": "
    Format(f fmt.State, c rune)  // Supports the %s, %m and %v verbs
    Code() (string, labels.Set)  // Error code and details
    Category() Category          // Resolved category; Internal if none was set
    Has(cause error) bool        // Deprecated: use Is
    Cause() error                // Direct cause
    RootCause() error            // Root cause in chain
    Chain() []error              // All errors in chain
}
Category Interface

A category classifies an error and carries an HTTP status code. Categories compare by identity, so two created with the same name are never equal.

type Category interface {
    Error() string   // The category name; a Category is itself an error
    StatusCode() int // Associated HTTP status code

    New(opts ...Option) error
    Newf(format string, args ...any) error
    Wrap(err error, opts ...Option) error
    Wrapf(err error, format string, args ...any) error
}

Register and look up custom categories with NewCategory(name, statusCode) and LookupCategory(name), which returns nil when the name is unknown.

Options

Customize errors using these options:

  • WithCode(code, details): Add error code and key-value details
  • WithMessage(format, args...): Set formatted error message
  • WithCategory(category): Set error category

What's New in v1.1.0

This release is backwards compatible. Existing code keeps compiling and behaving as it did.

errors.Is and errors.As from the standard library now traverse these errors. Errors from this package implement Unwrap() error, so a chain mixing these errors with any others can be walked from either side.

As is re-exported, so you need not import the standard errors package under an alias alongside this one.

Has is deprecated. Use Is, which performs the same search and additionally sees through Combine. Has still works and answers exactly as before.

- if errors.Has(err, originalErr) {
+ if errors.Is(err, originalErr) {

A bare category now matches itself. Category.New() with no options returns the category itself, which previously failed to match:

errors.Is(errors.NotFound.New(), errors.NotFound)  // was false, now true

Documentation

Index

Constants

This section is empty.

Variables

View Source
var (
	Cancaled = NewCategory("Cancelled", 499) // non-standard status code for cancellation

	BadRequest       = NewCategory("BadRequest", http.StatusBadRequest)
	InvalidArgument  = NewCategory("InvalidArgument", http.StatusBadRequest)
	Unauthorized     = NewCategory("Unauthorized", http.StatusUnauthorized)
	Forbidden        = NewCategory("Forbidden", http.StatusForbidden)
	PermissionDenied = NewCategory("PermissionDenied", http.StatusForbidden)
	NotFound         = NewCategory("NotFound", http.StatusNotFound)
	DeadlineExceeded = NewCategory("DeadlineExceeded", http.StatusRequestTimeout)
	Conflict         = NewCategory("Conflict", http.StatusConflict)
	AlreadyExist     = NewCategory("AlreadyExist", http.StatusConflict)
	TooManyRequests  = NewCategory("TooManyRequests", http.StatusTooManyRequests)

	Internal          = NewCategory("Internal", http.StatusInternalServerError)
	NotImplemented    = NewCategory("NotImplemented", http.StatusNotImplemented)
	Unavailable       = NewCategory("Unavailable", http.StatusServiceUnavailable)
	ResourceExhausted = NewCategory("ResourceExhausted", http.StatusServiceUnavailable)

	DBFailed = NewCategory("DBFailed", http.StatusInternalServerError)
)

Functions

func As added in v1.1.0

func As(err error, target any) bool

As finds the first error in err's tree whose concrete type is assignable to the value pointed to by target, sets target to that error, and reports whether one was found. Use it to recover a concrete error type and read its fields.

As adds nothing to the standard library's errors.As and exists only so that callers of this package need not also import errors under an alias, this package having taken the name.

As has nothing to do with categories. Unlike Is, it does not special-case a Category target, because a category is stored on an error rather than being an element of its tree. For a Category c, As(err, &c) reports only whether a Category value happens to sit in the chain: true for a bare NotFound.New(), false for NotFound.Newf("gone"), whose category is nonetheless NotFound. To test a category use Is; to read one use Error.Category.

func Combine

func Combine(errors ...error) error

Combine returns an error holding every non-nil error passed to it, or nil if there are none. The result implements Unwrap() []error, so errors.Is and errors.As examine each combined error in turn. Note that a combined error is a tree rather than a chain, and so carries no single category.

func Has deprecated

func Has(err error, cause error) bool

Has reports whether cause exists anywhere in err's tree.

Deprecated: use Is, which performs the same search. Has is retained for compatibility and calls the standard library's errors.Is directly. Unlike Is, it gives a Category target no special meaning, matching one only when the category value itself appears in the tree.

func Is

func Is(err error, target error) bool

Is reports whether err matches target. Which search it performs depends on the dynamic type of target, because the two are not variations of one idea.

If target is a Category, Is reports whether err resolves to that category. An error carries exactly one category, decided by the outermost error in the chain that carries one; see Error.Category. Is therefore resolves err to a single category and compares the two by identity. It does not search err's tree, and it never consults the standard library: a category is stored on an error rather than wrapped by it, so errors.Is cannot find one. A category held by an inner error is shadowed by one held further out, and categories from separate NewCategory calls are never equal, even given the same name.

Otherwise Is delegates to the standard library's errors.Is, which matches target against every error in err's tree. Use this form for sentinel errors.

func Message

func Message(err error) string

Message returns err.Error(), or the empty string if err is nil. For errors this package produced, that is every message in the chain, joined by ": ". To read only the outermost message, use Error.Message.

func New

func New(opts ...Option) error

New returns a new error configured by opts, capturing a stack trace at the call site. It never returns nil.

Always give an error a message. With no options, or with WithMessage(""), the error carries only its stack trace: it is non-nil, so callers take their error branch, but its Error method returns the empty string, so whatever logs it records nothing. Such an error reports that something failed while withholding what.

func Newf

func Newf(format string, args ...any) error

Newf returns a new error with a formatted message, capturing a stack trace at the call site. It is New with a single WithMessage option.

Never call Newf(""). The result is a non-nil error whose Error method returns the empty string: callers enter their error branch and log nothing. It also corrupts Combine, whose output for Combine(Newf(""), Newf("real")) is "; real". Give every error a message.

func Wrap

func Wrap(err error, opts ...Option) error

Wrap returns err wrapped with opts, or nil if err is nil, so it is safe to call on a value that may not be an error.

Given no options and an err this package already produced, Wrap returns err unchanged. Otherwise it returns a new error whose cause is err, capturing a stack trace only when err did not already carry one.

func Wrapf

func Wrapf(err error, format string, args ...any) error

Wrapf returns err wrapped with a formatted message, or nil if err is nil.

Never call Wrapf(err, ""). Unlike Wrap, Wrapf always applies an option, so an empty format does not yield err: it appends a message-less layer to the chain, which lengthens Chain and allocates while adding nothing a reader can see. When there is no context to add, call Wrap(err), which returns err untouched.

Types

type Category

type Category interface {
	Error() string
	StatusCode() int
	New(opts ...Option) error
	Newf(format string, args ...any) error
	Wrap(err error, opts ...Option) error
	Wrapf(err error, format string, args ...any) error
}

func LookupCategory added in v1.0.3

func LookupCategory(name string) Category

LookupCategory returns the category registered under name, or nil if no category has been registered under it.

func NewCategory

func NewCategory(category string, statusCode int) Category

NewCategory returns a new Category with the given name and HTTP status code, and registers it under that name for LookupCategory.

Categories compare by identity, so two calls with the same name and status code return categories that are not equal. Registering a name that is already taken replaces the earlier entry in the registry, but errors already carrying the earlier category keep it.

type Error

type Error interface {
	Message() string // latest error message without cause
	Error() string   // all error messages concatenated
	Format(f fmt.State, c rune)
	Code() (string, labels.Set)
	Category() Category
	Has(cause error) bool
	Cause() error
	RootCause() error
	Chain() []error
}

type Option

type Option func(*base)

func WithCategory

func WithCategory(category Category) Option

func WithCode

func WithCode(code string, details labels.Set) Option

func WithMessage

func WithMessage(format string, args ...any) Option

Jump to

Keyboard shortcuts

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