Documentation
¶
Overview ¶
Package xerr provides a structured, layer-aware error handling system for DDD / clean-architecture Go services.
Key features:
- Strongly typed, machine-readable error codes (Code), extensible with your own application-specific codes via RegisterCode
- A Kind (domain / application / infrastructure / unknown) that decides, by default, whether an error is safe to expose to a client — so infrastructure failures can be logged in full while only a generic response crosses the client boundary
- Field-level Violations with a closed, translatable Reason enum plus free-form Params for dynamic detail (e.g. a length bound)
- An optional, independent human-readable Message for when the backend should own the exact client-facing wording
- Internal-only Diagnostics for log context that never leaves the server
- JSON-safe responses (no internal leakage)
- Error wrapping compatible with errors.Is / errors.As
- A slog.LogValuer implementation, so passing an *Error straight to log/slog logs every field structured, no boilerplate
- Opt-in call-stack capture (WithStack) and a Recover helper that turns a recovered panic into an *Error with a stack attached
- DefaultMessage for a best-effort plain-English fallback where there is no frontend to build one
- Swagger-friendly error output model
What goes where ¶
Every Error field is always available server-side (for logging, errors.Is/errors.As, metrics); only a subset ever reaches the client, and that subset is chosen automatically by Kind:
field your logs (Error(), LogValue, accessors) the client (MarshalJSON)
---------- ------------------------------------------ --------------------------------
Code always always (real value if Exposed,
otherwise CodeInternalError)
Kind always never
Message always only if Exposed
Params always only if Exposed
Violations always only if Exposed
Diagnostics always never, even if Exposed
Err (cause) always never
Stack always, if captured never
Exposed defaults to Kind.Safe(): true for KindDomain / KindApplication, false for KindInfrastructure / KindUnknown. Override with WithExpose for the rare exception. See Error's doc comment for the full rationale.
Index ¶
- func ExposedCodes() map[Code]Kind
- func RegisterCode(code Code, kind Kind, httpStatus int)
- func RegisteredCodes() map[Code]Kind
- type Code
- type DiagnosticKey
- type Error
- func (e *Error) Code() Code
- func (e *Error) DefaultMessage() string
- func (e *Error) Diagnostics() map[DiagnosticKey]string
- func (e *Error) Err() error
- func (e *Error) Error() string
- func (e *Error) Exposed() bool
- func (e *Error) HTTPStatus() int
- func (e *Error) Is(target error) bool
- func (e *Error) Kind() Kind
- func (e *Error) LogValue() slog.Value
- func (e *Error) MarshalJSON() ([]byte, error)
- func (e *Error) Message() string
- func (e *Error) Params() map[string]any
- func (e *Error) Stack() string
- func (e *Error) Unwrap() error
- func (e *Error) Violations() []Violation
- type ErrorOption
- func WithDiagnostic(key DiagnosticKey, value string) ErrorOption
- func WithErr(err error) ErrorOption
- func WithExpose(expose bool) ErrorOption
- func WithKind(k Kind) ErrorOption
- func WithMessage(msg string) ErrorOption
- func WithParam(key string, value any) ErrorOption
- func WithStack() ErrorOption
- func WithViolation(field string, reason ErrorReason, params ...Param) ErrorOption
- func WithViolations(vs ...Violation) ErrorOption
- type ErrorReason
- type Kind
- type Param
- type SwaggerErrOutput
- type SwaggerViolationOutput
- type Violation
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func ExposedCodes ¶
ExposedCodes returns every registered Code whose default Kind is safe to expose to a client (Kind.Safe()) — built-in codes and anything added via RegisterCode alike. Use it to build an OpenAPI/Swagger enum for the response "code" field, or in a test asserting your application's public codes stay in sync with what's actually registered.
This reflects each Code's registered default only. An individual *Error can still override that default with WithExpose, so a code appearing here is not a guarantee every instance of it is exposed — only that it is by default.
func RegisterCode ¶
RegisterCode registers a new application Code with its Kind and HTTP status, so Code.Kind() / Code.HTTPStatus() — and everything built on them: Exposed(), MarshalJSON, ExposedCodes() — recognize it exactly like a built-in code.
xerr intentionally does not know about any single application's domain codes (e.g. a "PLAN_NOT_INCLUDED" or "USAGE_LIMIT_EXCEEDED" that only makes sense to one service). RegisterCode is how the owning application teaches xerr about them, typically from an init() so registration happens once, before the server accepts traffic:
const CodePlanNotIncluded xerr.Code = "PLAN_NOT_INCLUDED"
func init() {
xerr.RegisterCode(CodePlanNotIncluded, xerr.KindDomain, http.StatusForbidden)
}
RegisterCode panics if code is already registered — whether it's one of xerr's own built-in codes or one your application registered earlier — since a silent overwrite would change the Kind/status of an existing code out from under whatever already depends on it. Register each code exactly once; RegisterCode is not meant to be called from a hot path.
It also panics on malformed input, so a mistake fails loudly at startup instead of quietly registering a code no error will ever match correctly:
- code must be non-empty and match ^[A-Z][A-Z0-9_]*$ (upper-case letters, digits, and underscores, starting with a letter) — the same shape as every built-in code;
- kind must be one of the four defined Kind constants;
- httpStatus must fall in the 400-599 range (a registered code always represents a client- or server-side failure — there is no legitimate 2xx/3xx error code).
func RegisteredCodes ¶
RegisteredCodes returns every registered Code (built-in and anything added via RegisterCode) together with its default Kind — regardless of whether that Kind is safe to expose. Use ExposedCodes instead if you only want the subset that's safe to expose by default.
Types ¶
type Code ¶
type Code string
Code represents a machine-readable application error code.
const (
CodeTooManyRequests Code = "TOO_MANY_REQUESTS"
)
func (Code) HTTPStatus ¶
type DiagnosticKey ¶
type DiagnosticKey string
const ( DiagnosticOperation DiagnosticKey = "operation" DiagnosticReason DiagnosticKey = "reason" DiagnosticResource DiagnosticKey = "resource" )
func (DiagnosticKey) String ¶
func (d DiagnosticKey) String() string
type Error ¶
type Error struct {
// contains filtered or unexported fields
}
Error represents a structured application error.
An Error always carries its full detail in memory — every field below — so a single value works for both destinations: pass it to your logger directly (via Error(), or by logging Code/Kind/Diagnostics as structured fields) and pass the same value to json.Marshal for the HTTP response. The two destinations see different things on purpose:
field goes to your logs (Error() / accessors) goes to the client (MarshalJSON)
---------- ----------------------------------------- -------------------------------
Code always always — real code if Exposed,
otherwise CodeInternalError
Kind always never (not serialized)
Message always only if Exposed
Params always only if Exposed
Violations always only if Exposed
Diagnostics always never — not even when Exposed
Err (cause) always (via Error() / Unwrap()) never
Stack always, if captured (via Stack()) never
Exposed defaults from Kind: KindDomain and KindApplication are safe by default, KindInfrastructure and KindUnknown are not — see Kind.Safe. WithExpose overrides the default per error when a case genuinely needs it. Diagnostics are the one field with no exposure switch at all: they exist specifically for data that must never reach a client (internal identifiers, operation names, raw connection strings, ...), so treat WithDiagnostic as the "this can never leak" bucket and WithMessage / WithViolation as the "this is fine to leak, subject to Exposed" bucket.
func FromError ¶
FromError extracts an *Error from err's chain, if present. It is a thin convenience wrapper around errors.As, handy in HTTP middleware that needs to branch on whether an error is already an xerr.Error.
func New ¶
func New(code Code, opts ...ErrorOption) *Error
New creates a new structured xerr.Error. Its Kind defaults to the Code's registered Kind (see RegisterCode) and can be overridden with WithKind.
func Recover ¶
func Recover(v any, opts ...ErrorOption) *Error
Recover converts a recovered panic value into an *Error. Call it directly from a deferred recover:
defer func() {
if v := recover(); v != nil {
err = xerr.Recover(v)
}
}()
The result uses CodePanic (KindUnknown, unsafe to expose — the client only ever sees a generic internal error) and always carries a captured stack (see Stack), since a stack trace is the entire reason to catch a panic in the first place. Returns nil if v is nil (recover() is commonly called unconditionally; a nil v means there was no panic).
func Wrap ¶
func Wrap(err error, code Code, opts ...ErrorOption) *Error
Wrap converts a raw error into an xerr.Error with a given code, preserving err for Unwrap/errors.Is/errors.As and log output. Its Kind defaults to the Code's registered Kind (see RegisterCode) and can be overridden with WithKind. Returns nil if err is nil.
func (*Error) Code ¶
Code returns the machine-readable error code. Safe to log always; safe to return to a client always (MarshalJSON substitutes CodeInternalError for the real value when the error is not Exposed).
func (*Error) DefaultMessage ¶
DefaultMessage returns a best-effort human-readable message: the explicit Message if one was set, otherwise each Violation's DefaultMessage joined together, otherwise a generic fallback based on Kind. Same scope note as Violation.DefaultMessage: a fallback for contexts with no frontend to build their own copy, not a replacement for one that has.
func (*Error) Diagnostics ¶
func (e *Error) Diagnostics() map[DiagnosticKey]string
Diagnostics returns a copy of the internal-only debug context attached to this error (e.g. which operation was running, an internal resource id). Log-only, unconditionally: unlike Message and Violations, diagnostics have no Exposed switch and are never serialized by MarshalJSON no matter what — put here anything that must never reach a client.
func (*Error) Err ¶
Err returns the wrapped underlying cause, if any. Log-only: include it in your logger output (or call Error(), which already does), but never serialize it to a client — it commonly holds raw driver/library errors (SQL, HTTP client, ...) that can leak internal topology.
func (*Error) Exposed ¶
Exposed reports whether this error's Message and Violations are safe to return to a client. It defaults to Kind.Safe() and can be overridden per-error with WithExpose.
func (*Error) HTTPStatus ¶
HTTPStatus returns the HTTP status associated with this error's Code.
func (*Error) Is ¶
Is reports whether target is an *Error with the same Code. Message, Violations, and diagnostics are runtime detail and intentionally excluded, so xerr.New(xerr.CodeNotFound) works as a sentinel for errors.Is regardless of what detail a concrete instance carries.
func (*Error) Kind ¶
Kind returns the error's classification. Log-only: never sent to a client, and not part of MarshalJSON's output.
func (*Error) LogValue ¶
LogValue implements slog.LogValuer. Passing an *Error to log/slog renders every field your logger should see — Code, Kind, Message, Params, Violations, Diagnostics, the wrapped Err, and Stack if captured — as structured attributes, without hand-writing each one at the call site:
slog.Error("request failed", "err", xerr.Wrap(dbErr, xerr.CodeDatabaseError))
This is deliberately the log-only view: unlike MarshalJSON, it never consults Exposed and always includes Kind, Diagnostics, and Err — the exact fields the client boundary hides — because this path is for your logger, never for a client response.
func (*Error) MarshalJSON ¶
MarshalJSON produces the client-safe JSON representation of the error. Only four fields are ever eligible to appear: code, message, params, and violations — Kind, Diagnostics, and the wrapped Err are never serialized, under any circumstance.
When Exposed is false, the response collapses further, to a bare {"code":"INTERNAL_SERVER_ERROR"}: the specific code, message, params, and any wrapped error stay available server-side via Error(), Code(), Params(), and Diagnostics(), but never reach the client.
func (*Error) Message ¶
Message returns the human-readable message, if any. Safe to log always. Only sent to a client when Exposed is true.
func (*Error) Params ¶
Params returns a copy of the error-level dynamic detail attached via WithParam (e.g. which Resource was involved, or a limit that was exceeded). Safe to log always. Only sent to a client when Exposed is true.
func (*Error) Stack ¶
Stack returns the call stack captured by WithStack or Recover, if any, formatted one frame per line as "function\n\tfile:line". Returns "" if no stack was captured. Log-only: never part of MarshalJSON's output, and deliberately excluded from Error()'s compact single-line output — call Stack() explicitly (or log the *Error via slog, whose LogValue includes it) when you want it.
func (*Error) Violations ¶
Violations returns a copy of the field-level violations attached to this error. Safe to log always. Only sent to a client when Exposed is true.
type ErrorOption ¶
type ErrorOption func(*Error)
ErrorOption is a functional constructor modifier.
func WithDiagnostic ¶
func WithDiagnostic(key DiagnosticKey, value string) ErrorOption
WithDiagnostic attaches internal-only debug context (e.g. the operation being performed, or a resource identifier). Diagnostics appear in Error()'s log output but are never exposed via MarshalJSON.
func WithErr ¶
func WithErr(err error) ErrorOption
WithErr attaches the underlying error being wrapped, preserving it for Unwrap/errors.Is/errors.As and for Error()'s log output. It is never exposed via MarshalJSON.
func WithExpose ¶
func WithExpose(expose bool) ErrorOption
WithExpose forces whether Message and Violations are safe to return to a client, overriding the Kind-based default. Use this for the rare exception: an infrastructure error that is actually safe to describe, or a domain error that happens to carry sensitive detail.
func WithKind ¶
func WithKind(k Kind) ErrorOption
WithKind overrides the Kind the error would otherwise inherit from its Code (see Code.Kind and RegisterCode), and with it, the default answer to Exposed.
func WithMessage ¶
func WithMessage(msg string) ErrorOption
WithMessage sets a ready-to-display, human-readable message. Use this when the backend should own the exact wording the client shows. Independent of WithViolation — set either, both, or neither.
func WithParam ¶
func WithParam(key string, value any) ErrorOption
WithParam attaches error-level dynamic detail a client-facing message template needs — analogous to a Violation's Params, but scoped to the whole error rather than one field. Use it for things like which resource was involved or a numeric limit that was exceeded (e.g. WithParam("resource", "product"), WithParam("max", 5)). Sent to the client only when Exposed — exactly like WithMessage and WithViolation. Can be called multiple times; a later call with the same key overwrites the earlier value.
func WithStack ¶
func WithStack() ErrorOption
WithStack captures the current call stack, for later inspection via Stack(). Opt-in and skipped by default: runtime.Callers is cheap but not free, and errors constructed in a hot path (e.g. per-field validation, run once per request) shouldn't pay for it unasked. Reach for it on the errors you'll actually want a trace for — typically KindInfrastructure / KindUnknown ones. Recover always captures a stack, since that is the entire point of catching a panic.
func WithViolation ¶
func WithViolation(field string, reason ErrorReason, params ...Param) ErrorOption
WithViolation adds a field-level violation: a stable, translatable Reason plus optional Params for the dynamic values a message template needs (e.g. P("min", 8)). Use this when the client should build its own localized message. Independent of WithMessage — set either, both, or neither. Can be called multiple times to report several violations.
func WithViolations ¶
func WithViolations(vs ...Violation) ErrorOption
WithViolations appends a batch of already-built violations in one call — handy when a third-party validator (e.g. go-playground/validator) already produced its own list and you're translating it into xerr.Violation instead of building each one by hand with WithViolation. Combines with WithViolation; both are additive.
type ErrorReason ¶
type ErrorReason string
ErrorReason is a stable, translatable identifier for why a single field violated a rule. It intentionally stays a closed enum: the frontend can build a static localization table keyed by these values. Anything that varies per-occurrence (a length bound, an allowed set, ...) belongs in a Violation's Params instead of growing this enum.
const ( ErrorReasonRequired ErrorReason = "required" ErrorReasonInvalidFormat ErrorReason = "invalid_format" ErrorReasonInvalidValue ErrorReason = "invalid_value" // params: allowed ErrorReasonTooShort ErrorReason = "too_short" // params: min ErrorReasonTooLong ErrorReason = "too_long" // params: max ErrorReasonTooSmall ErrorReason = "too_small" // params: min ErrorReasonTooLarge ErrorReason = "too_large" // params: max ErrorReasonMismatch ErrorReason = "mismatch" ErrorReasonAlreadyExists ErrorReason = "already_exists" ErrorReasonNotFound ErrorReason = "not_found" ErrorReasonCorrupted ErrorReason = "corrupted" ErrorReasonExpired ErrorReason = "expired" )
func (ErrorReason) String ¶
func (e ErrorReason) String() string
type Kind ¶
type Kind string
Kind classifies where an error originated, which in turn decides whether it is safe to expose to a client by default.
const ( // KindDomain marks a violation of a core business/domain rule tied to a // specific entity or invariant (e.g. "order already shipped", "user not // found"). Safe to expose by default. KindDomain Kind = "domain" // KindApplication marks a cross-cutting application-layer concern that // is not about a specific domain entity (auth, authorization, rate // limiting, request-contract validation). Safe to expose by default. KindApplication Kind = "application" // KindInfrastructure marks a failure from an external system the // service depends on (database, cache, queue, network, third-party // API). Never safe to expose by default: the client should only see // that something failed, while full detail stays in server-side logs. KindInfrastructure Kind = "infrastructure" // KindUnknown marks an unclassified or unexpected error. Treated the // same as KindInfrastructure: unsafe to expose by default. KindUnknown Kind = "unknown" )
type Param ¶
Param is a single key/value entry attached to a Violation. Build one with P and pass it to WithViolation.
type SwaggerErrOutput ¶
type SwaggerErrOutput struct {
Code string `json:"code" example:"VALIDATION_FAILED"`
Message string `json:"message,omitempty" example:"invalid request body"`
Params map[string]any `json:"params,omitempty" swaggertype:"object" example:"resource:product"`
Violations []SwaggerViolationOutput `json:"violations,omitempty"`
} // @name Error
SwaggerErrOutput represents the public JSON structure of xerr.Error.
This is used only for Swagger and documentation. Actual API responses are produced by MarshalJSON of xerr.Error.
Code is typed as a plain string here, not an enum: xerr only knows its own built-in codes plus whatever the owning application registers via RegisterCode, so it cannot bake a closed set into this struct without also knowing every domain code the application defines. If you want "code" to render as an OpenAPI enum, build the value list yourself — combine xerr.ExposedCodes() with your own domain codes — and apply it as a swaggo `enums:"..."` tag (or an equivalent doc-generation step) on your own copy of this struct.
type SwaggerViolationOutput ¶
type SwaggerViolationOutput struct {
Field string `json:"field" example:"email"`
Reason string `json:"reason" example:"invalid_format"`
Params map[string]any `json:"params,omitempty" swaggertype:"object" example:"min:8"`
} // @name ErrorViolation
SwaggerViolationOutput represents the public JSON structure of a xerr.Violation, for Swagger and documentation purposes only.
type Violation ¶
type Violation struct {
Field string `json:"field"`
Reason ErrorReason `json:"reason"`
Params map[string]any `json:"params,omitempty"`
}
Violation describes a single field-level rule violation.
Reason is the stable, translatable identifier the client uses to look up a localized message template (see ErrorReason). Params carries the dynamic values that template needs — e.g. {"min": 8} alongside ErrorReasonTooShort — and is the only place free-form data belongs: Reason itself must stay within the fixed ErrorReason enum so a frontend translation table never goes stale.
func (Violation) DefaultMessage ¶
DefaultMessage renders a plain-English fallback sentence for a Violation, substituting Params where its template needs them.
This is not a localization system: there is no catalog, no locale negotiation, nothing pluggable. It exists for the contexts that have no frontend to own translation — a CLI tool, a server log meant for a human, a quick prototype. Wherever there is a real client, prefer letting it build its own copy from Reason + Params (that's what the enum is for); reach for DefaultMessage only as a fallback.