Help us shape the future of Go! Take the 2026 Go Developer Survey

hes

package module
v1.0.0 Latest Latest
Warning

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

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

README

hes

Build Status

Structured HTTP errors for Go.

hes v1 is a clean break from v0: standard errors chain support, options-based construction, immutable helpers, and safe JSON responses.

Install

go get github.com/vicanso/hes@v1.0.0

Requires Go 1.22+.

Quick start

package main

import (
	"fmt"
	"net/http"

	"github.com/vicanso/hes"
)

func main() {
	err := hes.New("invalid email",
		hes.WithCode("user.invalid_email"),
		hes.WithCategory("user"),
		hes.WithStatus(http.StatusUnprocessableEntity),
		hes.WithExtra("field", "email"),
	)

	fmt.Println(err)
	// statusCode=422, category=user, code=user.invalid_email, message=invalid email

	buf, _ := err.MarshalPublic()
	fmt.Println(string(buf))
}

Construct

Function Default status Notes
New(msg, opts...) 400 Client-facing errors
Wrap(err, opts...) 500 + Exception Unknown errors; uses errors.As — chain *Error is extracted, not re-wrapped
From(err) — errors.As then Wrap
BadRequest 400 Alias of New
Unauthorized / Forbidden / NotFound / RequestTimeout / Conflict / UnsupportedMediaType / UnprocessableEntity / TooManyRequests 4xx Thin helpers
Internal / BadGateway / ServiceUnavailable / GatewayTimeout 5xx + Exception Server failures
Options
hes.WithStatus(403)
hes.WithCode("auth.forbidden")
hes.WithCategory("auth")
hes.WithSubCategory("token")
hes.WithTitle("Forbidden")
hes.WithException()
hes.WithExtra("userId", 1)
hes.WithExtras(map[string]any{"a": 1})
hes.WithCause(err)
hes.WithCaller(0) // capture call site
Immutable updates

Prefer deriving new values over mutating fields:

base := hes.NotFound("user not found", hes.WithCode("user.not_found"))
err := base.WithExtra("id", userID).WithMessage("user 42 not found")
Aggregate validation errors
err := hes.BadRequest("validation failed", hes.WithCode("validation")).
	Append(
		hes.New("email required", hes.WithCode("field.email")),
		hes.New("name too short", hes.WithCode("field.name")),
	)

Standard library interop

cause := errors.New("db timeout")
err := hes.Wrap(cause, hes.WithCode("db.timeout"))

errors.Is(err, cause)  // true via Unwrap
hes.Is(err)            // true — chain contains *hes.Error
he, ok := hes.As(err)  // extract *hes.Error

// Match by application code (both sides need non-empty Code)
sentinel := &hes.Error{Code: "db.timeout"}
errors.Is(err, sentinel) // true

// Code-less: match when client-facing fields are equal
a := &hes.Error{StatusCode: 404, Category: "user", Message: "not found"}
b := &hes.Error{StatusCode: 404, Category: "user", Message: "not found"}
errors.Is(a, b) // true — same data

errors.Is / (*Error).Is rules (also used by Append dedup):

  1. Pointer identity (handled by the standard library first)
  2. Both have non-empty Code → codes equal
  3. Otherwise → same StatusCode, Category, SubCategory, Code, Title, Message

JSON responses

  • ToJSON() — full error (may include file / line)
  • MarshalPublic() — client view, no tree clone (strips caller)
  • Public() — deep clone with caller stripped (when you need an *Error)
  • WriteJSON(w) / WriteJSONTo(w) — public JSON; nil receiver returns hes.ErrNil
func handle(w http.ResponseWriter, r *http.Request) {
	err := doWork()
	if err != nil {
		he := hes.From(err)
		_ = he.WriteJSON(w)
		return
	}
	w.WriteHeader(http.StatusNoContent)
}

Example public payload:

{
  "statusCode": 400,
  "code": "user.invalid_email",
  "category": "user",
  "message": "invalid email",
  "extra": {"field": "email"}
}

Caller capture

// Per error
err := hes.New("boom", hes.WithCaller(0))

// Global default for New/Wrap
hes.SetDefaultCaller(true)
hes.SetFileConverter(func(file string) string {
	return strings.TrimPrefix(file, "/app/")
})

Migration from v0

v1 removes the combinatorial NewWith* API and mutex helpers.

v0 v1
New(msg, category) New(msg, WithCategory(category))
NewWithStatusCode(msg, code, cat) New(msg, WithStatus(code), WithCategory(cat))
NewWithError(err) Wrap(err) (default 500, not 400)
NewWithErrorStatusCode(err, code) Wrap(err, WithStatus(code))
NewWithCaller(msg) New(msg, WithCaller(0))
NewWithException(msg) New(msg, WithException()) or Internal(msg)
NewMutex(...) removed — treat errors as immutable
he.Add(err) he = he.Append(err)
he.AddExtra(k, v) he = he.WithExtra(k, v)
he.CloneWithMessage(m) he.WithMessage(m)
IsError(err) Is(err) / errors.As
he.Err he.Cause
he.ToJSON() → []byte he.ToJSON() → ([]byte, error)
EnableCaller(true) SetDefaultCaller(true)
SetFileConvertor(fn) SetFileConverter(fn)
Semantic changes
  1. Wrap defaults to 500 + Exception (v0 used 400).
  2. Wrap / From walk the error chain via errors.As (do not double-wrap *Error).
  3. Clone / With* deep-copy Extra and Errs; Append shares leaf pointers under the immutability convention.
  4. No mutex API — do not mutate errors across goroutines; derive new values.
  5. errors.Is matches by Code, or by structural fields when Code is empty (aligned with Append dedup).
  6. ToJSON() returns ([]byte, error); prefer MarshalPublic / WriteJSON for clients.

License

Apache-2.0

Documentation

Index

Constants

This section is empty.

Variables

View Source
var ErrNil = errors.New("hes: nil error")

ErrNil is returned when a method is called on a nil *Error receiver that cannot produce a meaningful HTTP response.

Functions

func Is added in v1.0.0

func Is(err error) bool

Is reports whether err's chain contains an *Error.

func SetDefaultCaller added in v1.0.0

func SetDefaultCaller(enabled bool)

SetDefaultCaller enables or disables automatic caller capture for New/Wrap.

func SetFileConverter added in v1.0.0

func SetFileConverter(fn FileConverter)

SetFileConverter sets a path rewriter used when capturing callers. Pass nil to clear.

Types

type Error

type Error struct {
	// StatusCode is the HTTP status code.
	StatusCode int `json:"statusCode,omitempty"`
	// Code is an application-defined error code.
	Code string `json:"code,omitempty"`
	// Category groups related errors (e.g. package or domain name).
	Category string `json:"category,omitempty"`
	// SubCategory further classifies the error within Category.
	SubCategory string `json:"subCategory,omitempty"`
	// Title is a short human-readable summary.
	Title string `json:"title,omitempty"`
	// Message is the primary error description.
	Message string `json:"message,omitempty"`
	// Exception marks unexpected server-side failures.
	Exception bool `json:"exception,omitempty"`
	// File is the source file from caller capture, if enabled.
	File string `json:"file,omitempty"`
	// Line is the source line from caller capture, if enabled.
	Line int `json:"line,omitempty"`
	// Extra holds optional structured metadata.
	Extra map[string]any `json:"extra,omitempty"`
	// Errs holds aggregated sub-errors (e.g. validation).
	Errs []*Error `json:"errs,omitempty"`
	// Cause is the underlying error, omitted from JSON.
	Cause error `json:"-"`
}

Error is a structured HTTP error.

After construction, treat values as immutable: use With* / Append / Clone to derive new instances instead of mutating fields in place.

func As added in v1.0.0

func As(err error) (*Error, bool)

As extracts the first *Error from err's chain.

func BadGateway added in v1.0.0

func BadGateway(message string, opts ...Option) *Error

BadGateway creates a 502 exception error.

func BadRequest added in v1.0.0

func BadRequest(message string, opts ...Option) *Error

BadRequest creates a 400 error (alias of New).

func Conflict added in v1.0.0

func Conflict(message string, opts ...Option) *Error

Conflict creates a 409 error.

func Forbidden added in v1.0.0

func Forbidden(message string, opts ...Option) *Error

Forbidden creates a 403 error.

func From added in v1.0.0

func From(err error) *Error

From returns err as *Error using errors.As when possible; otherwise Wrap(err).

func GatewayTimeout added in v1.0.0

func GatewayTimeout(message string, opts ...Option) *Error

GatewayTimeout creates a 504 exception error.

func Internal added in v1.0.0

func Internal(message string, opts ...Option) *Error

Internal creates a 500 exception error.

func New

func New(message string, opts ...Option) *Error

New creates a client-facing HTTP error (default status 400).

func NotFound added in v1.0.0

func NotFound(message string, opts ...Option) *Error

NotFound creates a 404 error.

func RequestTimeout added in v1.0.0

func RequestTimeout(message string, opts ...Option) *Error

RequestTimeout creates a 408 error.

func ServiceUnavailable added in v1.0.0

func ServiceUnavailable(message string, opts ...Option) *Error

ServiceUnavailable creates a 503 exception error.

func TooManyRequests added in v1.0.0

func TooManyRequests(message string, opts ...Option) *Error

TooManyRequests creates a 429 error.

func Unauthorized added in v1.0.0

func Unauthorized(message string, opts ...Option) *Error

Unauthorized creates a 401 error.

func UnprocessableEntity added in v1.0.0

func UnprocessableEntity(message string, opts ...Option) *Error

UnprocessableEntity creates a 422 error.

func UnsupportedMediaType added in v1.0.0

func UnsupportedMediaType(message string, opts ...Option) *Error

UnsupportedMediaType creates a 415 error.

func Wrap

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

Wrap converts err into an *Error.

  • nil → nil
  • error chain containing *Error, no opts → that *Error (same pointer)
  • error chain containing *Error, with opts → deep clone with options applied
  • other errors → new *Error with status 500, Exception=true, Cause=err

func (*Error) Append added in v1.0.0

func (e *Error) Append(errs ...error) *Error

Append returns a clone with additional sub-errors. Nil errors are skipped. Nested multi-errors are flattened. Duplicates (same logical error per Is/same) are ignored.

Sub-error pointers are shared (errors are treated as immutable). Use Clone on a child first if you need an isolated copy.

func (*Error) Clone

func (e *Error) Clone() *Error

Clone returns a deep copy of the error (Extra map and Errs tree).

func (*Error) Error

func (e *Error) Error() string

Error implements the error interface.

func (*Error) Format

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

Format implements fmt.Formatter.

%s, %v   — Error()
%q       — quoted Message
%+v      — Error() plus cause chain and call site

func (*Error) Is added in v1.0.0

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

Is reports whether target matches this error for errors.Is.

Matching rules:

  • pointer identity is handled by errors.Is before calling Is
  • if both sides have a non-empty Code: Codes are equal
  • otherwise: same StatusCode, Category, SubCategory, Code, Title, and Message

The structural rule matches Append dedup so identical client-facing data is treated as the same error.

func (*Error) IsEmpty

func (e *Error) IsEmpty() bool

IsEmpty reports whether there are no sub-errors.

func (*Error) IsNotEmpty

func (e *Error) IsNotEmpty() bool

IsNotEmpty reports whether there is at least one sub-error.

func (*Error) MarshalPublic added in v1.0.0

func (e *Error) MarshalPublic() ([]byte, error)

MarshalPublic marshals the client-facing view (no file/line) without cloning.

func (*Error) Public added in v1.0.0

func (e *Error) Public() *Error

Public returns a deep clone safe for client responses: caller File/Line are stripped from the error and all sub-errors.

Prefer MarshalPublic / WriteJSON when you only need JSON output; those paths avoid a full tree clone.

func (*Error) StatusOr added in v1.0.0

func (e *Error) StatusOr(fallback int) int

StatusOr returns StatusCode, or fallback when StatusCode is zero.

func (*Error) StatusOrInternal added in v1.0.0

func (e *Error) StatusOrInternal() int

StatusOrInternal returns StatusCode, or 500 when zero.

func (*Error) ToJSON

func (e *Error) ToJSON() ([]byte, error)

ToJSON marshals the full error (including caller info when present).

func (*Error) Unwrap added in v1.0.0

func (e *Error) Unwrap() error

Unwrap returns the underlying cause, or joined sub-errors when present.

func (*Error) WithCategory added in v1.0.0

func (e *Error) WithCategory(category string) *Error

WithCategory returns a clone with an updated category.

func (*Error) WithCause added in v1.0.0

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

WithCause returns a clone with an updated cause.

func (*Error) WithCode added in v1.0.0

func (e *Error) WithCode(code string) *Error

WithCode returns a clone with an updated application error code.

func (*Error) WithException added in v1.0.0

func (e *Error) WithException(exception bool) *Error

WithException returns a clone with Exception set.

func (*Error) WithExtra added in v1.0.0

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

WithExtra returns a clone with one extra field set.

func (*Error) WithMessage added in v1.0.0

func (e *Error) WithMessage(message string) *Error

WithMessage returns a clone with an updated message.

func (*Error) WithStatus added in v1.0.0

func (e *Error) WithStatus(statusCode int) *Error

WithStatus returns a clone with an updated HTTP status code.

func (*Error) WithSubCategory added in v1.0.0

func (e *Error) WithSubCategory(subCategory string) *Error

WithSubCategory returns a clone with an updated sub-category.

func (*Error) WithTitle added in v1.0.0

func (e *Error) WithTitle(title string) *Error

WithTitle returns a clone with an updated title.

func (*Error) WriteJSON added in v1.0.0

func (e *Error) WriteJSON(w http.ResponseWriter) error

WriteJSON writes the public JSON body and status to w. A nil receiver returns ErrNil and writes nothing.

func (*Error) WriteJSONTo added in v1.0.0

func (e *Error) WriteJSONTo(w io.Writer) error

WriteJSONTo marshals the public error to any writer without a full clone. A nil receiver returns ErrNil and writes nothing.

type FileConverter added in v1.0.0

type FileConverter func(file string) string

FileConverter rewrites captured source paths (e.g. strip build prefixes).

type Option added in v1.0.0

type Option func(*Error)

Option configures an Error at construction time.

func WithCaller added in v1.0.0

func WithCaller(skip int) Option

WithCaller captures the call site when the option is created. skip is additional frames above the WithCaller call itself (0 = direct caller).

func WithCategory added in v1.0.0

func WithCategory(category string) Option

WithCategory sets the error category.

func WithCause added in v1.0.0

func WithCause(cause error) Option

WithCause sets the underlying cause.

func WithCode added in v1.0.0

func WithCode(code string) Option

WithCode sets the application error code.

func WithException added in v1.0.0

func WithException() Option

WithException marks the error as an unexpected exception.

func WithExtra added in v1.0.0

func WithExtra(key string, value any) Option

WithExtra sets a single extra metadata entry.

func WithExtras added in v1.0.0

func WithExtras(extra map[string]any) Option

WithExtras merges metadata entries into Extra.

func WithStatus added in v1.0.0

func WithStatus(statusCode int) Option

WithStatus sets the HTTP status code.

func WithSubCategory added in v1.0.0

func WithSubCategory(subCategory string) Option

WithSubCategory sets the error sub-category.

func WithTitle added in v1.0.0

func WithTitle(title string) Option

WithTitle sets the error title.

Jump to

Keyboard shortcuts

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