errors

package
v0.0.4 Latest Latest
Warning

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

Go to latest
Published: Aug 24, 2026 License: MIT Imports: 5 Imported by: 0

Documentation

Overview

Package errors defines Forge's error contract: a portable representation that carries a failure's category, identity, and cause across process boundaries without losing meaning.

An error is three separable things, and this package keeps them separate:

  • domain: what kind of failure this is, carried by Kind;
  • identity: which specific failure and whose, carried by domain and reason;
  • diagnosis: why it happened, carried by the wrapped cause and a trace ID.

A transport status code is none of these. It is a projection of Kind, computed at the boundary, so that constructing an error never requires knowing which transport will carry it.

Errors that form part of a service contract are declared in Protobuf and generated by protoc-gen-go-errors as immutable package-level sentinels. Deriving from a sentinel never mutates it:

return v1.ErrSessionNotFound.Msgf("session %q", id).Wrap(cause)

Matching uses the standard library. This package adds no vocabulary of its own for the purpose:

if errors.Is(err, v1.ErrSessionNotFound) { ... }
Example (ForeignError)

Example_foreignError mirrors the guide's warning: a bare non-Forge error arrives at the boundary as KindUnknown with nothing to grep for.

package main

import (
	"fmt"

	"github.com/sylphylabs/forge/errors"
)

func main() {
	err := fmt.Errorf("some dependency failed")

	fmt.Println(errors.KindOf(err))
	fmt.Println(errors.ReasonOf(err) == "")
}
Output:
UNKNOWN
true
Example (Inspecting)

Example_inspecting mirrors "Inspecting an error": the standard library vocabulary plus KindOf for classification-only matching.

package main

import (
	"fmt"

	"github.com/sylphylabs/forge/errors"
)

// ErrNotFound mirrors the sentinel protoc-gen-go-errors emits for a Protobuf
// error enum value ("Declaring contract errors in Protobuf" in the guide).
var ErrNotFound = errors.MustDefine(
	errors.KindNotFound,
	"sylphy.test.v1",
	"FAILURE_REASON_NOT_FOUND",
)

func main() {
	err := error(ErrNotFound.Msg("document \"42\" not found"))

	if errors.Is(err, ErrNotFound) {
		fmt.Println("matched by identity")
	}

	var e *errors.Error
	if errors.As(err, &e) {
		fmt.Println(e.Domain())
	}

	switch errors.KindOf(err) {
	case errors.KindNotFound:
		fmt.Println("not found")
	case errors.KindUnavailable:
		fmt.Println("unavailable")
	}
}
Output:
matched by identity
sylphy.test.v1
not found
Example (LocalFailure)

Example_localFailure mirrors the guide's process-local error: a failure that never leaves the process needs no Protobuf declaration.

package main

import (
	"fmt"

	"github.com/sylphylabs/forge/errors"
)

func main() {
	cause := fmt.Errorf("checksum mismatch")

	err := error(errors.Of(errors.KindInternal).WithReason("CACHE_CORRUPT").Wrap(cause))

	fmt.Println(errors.KindOf(err))
	fmt.Println(errors.ReasonOf(err))
}
Output:
INTERNAL
CACHE_CORRUPT
Example (Public)

Example_public mirrors "Choosing what to disclose": a transport serializes only PublicOf(err), and FromPublic rebuilds a remote error from the same facts — with no cause.

package main

import (
	"fmt"

	"github.com/sylphylabs/forge/errors"
)

// ErrNotFound mirrors the sentinel protoc-gen-go-errors emits for a Protobuf
// error enum value ("Declaring contract errors in Protobuf" in the guide).
var ErrNotFound = errors.MustDefine(
	errors.KindNotFound,
	"sylphy.test.v1",
	"FAILURE_REASON_NOT_FOUND",
)

func main() {
	cause := fmt.Errorf("dsn: postgres://user:hunter2@db/prod")
	err := error(ErrNotFound.Msg("document not found").Wrap(cause))

	public := errors.PublicOf(err)
	fmt.Println(public.Kind, public.Domain, public.Reason)

	remote := errors.FromPublic(public)
	fmt.Println(errors.Is(remote, ErrNotFound))
	fmt.Println(errors.Unwrap(remote) == nil) // the cause chain does not cross the boundary
}
Output:
NOT_FOUND sylphy.test.v1 FAILURE_REASON_NOT_FOUND
true
true
Example (Returning)

Example_returning mirrors "Returning an error": deriving from an immutable sentinel with a message, metadata, and a wrapped cause.

package main

import (
	"fmt"

	"github.com/sylphylabs/forge/errors"
)

// ErrNotFound mirrors the sentinel protoc-gen-go-errors emits for a Protobuf
// error enum value ("Declaring contract errors in Protobuf" in the guide).
var ErrNotFound = errors.MustDefine(
	errors.KindNotFound,
	"sylphy.test.v1",
	"FAILURE_REASON_NOT_FOUND",
)

func main() {
	name := "answers/42"
	tenantID := "acme"
	cause := fmt.Errorf("stale replica")

	err := error(ErrNotFound.
		Msgf("document %q", name).
		Meta("tenant", tenantID).
		Wrap(cause))

	fmt.Println(errors.Is(err, ErrNotFound))
	fmt.Println(errors.Unwrap(err) == cause)
}
Output:
true
true
Example (Violations)

Example_violations mirrors "Aggregating field failures": a validation pass reports everything it found, and Err returns nil when nothing was recorded.

package main

import (
	"fmt"

	"github.com/sylphylabs/forge/errors"
)

func main() {
	age := -3

	var v errors.Violations
	v.Add("email", "malformed")
	v.Addf("age", "must be positive, got %d", age)
	err := v.Err(errors.KindInvalidArgument)

	fmt.Println(errors.KindOf(err))
	fmt.Println(len(errors.FromError(err).Violations()))

	var empty errors.Violations
	fmt.Println(empty.Err(errors.KindInvalidArgument) == nil)
}
Output:
INVALID_ARGUMENT
2
true

Index

Examples

Constants

View Source
const Domain = "forge.sylphylabs.io"

Domain namespaces the reasons owned by the framework itself, so that a caller can tell a failure raised by Forge from one raised by the service it carries.

View Source
const SupportPackageIsVersion1 = true

SupportPackageIsVersion1 is referenced from generated *_errors.pb.go files to assert that they are compiled against a runtime that supports them. When a release changes what generated code requires, the constant it references changes with it, so a stale pairing fails to compile instead of misbehaving.

It should not be referenced from any other code.

Variables

View Source
var ErrUnsupported = stderrors.ErrUnsupported

ErrUnsupported indicates that a requested operation cannot be performed, because it is unsupported. For example, a call to os.Link when using a file system that does not support hard links.

Functions and methods should not return this error but should instead return an error including appropriate context that satisfies

errors.Is(err, errors.ErrUnsupported)

either by directly wrapping ErrUnsupported or by implementing an Is method.

Functions and methods should document the cases in which an error wrapping this will be returned.

Functions

func As

func As(err error, target any) bool

As finds the first error in err's chain that matches target, and if so, sets target to that error value and returns true.

The chain consists of err itself followed by the sequence of errors obtained by repeatedly calling Unwrap.

An error matches target if the error's concrete value is assignable to the value pointed to by target, or if the error has a method As(interface{}) bool such that As(target) returns true. In the latter case, the As method is responsible for setting target.

As will panic if target is not a non-nil pointer to either a type that implements error, or to any interface type. As returns false if err is nil.

func DomainOf

func DomainOf(err error) string

DomainOf returns the domain of any error, or the empty string when it has none.

func Is

func Is(err, target error) bool

Is reports whether any error in err's chain matches target.

The chain consists of err itself followed by the sequence of errors obtained by repeatedly calling Unwrap.

An error is considered to match a target if it is equal to that target or if it implements a method Is(error) bool such that Is(target) returns true.

func IsContract added in v0.0.3

func IsContract(domain, reason string) bool

IsContract reports whether the identity pair was declared through MustDefine in this process.

It answers "may this identity disclose itself through PublicOf?" — the same registry consultation the projection performs. Runtime assertion uses it to separate an undeclared contract identity, which would leave the process fully disclosed, from an anonymous local failure, which projects as an internal error anyway.

func IsUndisclosed added in v0.0.3

func IsUndisclosed(err error) bool

IsUndisclosed reports whether err carries the Undisclose marker.

func Join

func Join(errs ...error) error

Join returns an error that wraps the given errors. The returned error has a method Unwrap() []error that returns the given errors in order.

Join returns nil if errs contains no non-nil error values. If errs contains a single non-nil error value, Join returns that error. If errs contains multiple non-nil error values, Join returns an error that formats as the concatenation of the format of the non-nil error values, separated by "; ". The returned error's Unwrap method returns a slice of the non-nil error values.

Join is designed for use in situations where multiple errors may be returned, such as when processing a list of items and collecting errors from each item. It allows you to combine those errors into a single error value that can be returned to the caller.

func ReasonOf

func ReasonOf(err error) string

ReasonOf returns the reason of any error, or the empty string when it has none.

func Undisclose added in v0.0.3

func Undisclose(err error) error

Undisclose returns err with its public data marked as not disclosable.

PublicOf projects the result as an internal failure carrying only its trace ID, regardless of every other disclosure rule — a declared contract identity and even a remote pass-through are overridden, because Undisclose is a deliberate verdict about this occurrence, not a property of the identity. Everything else about the error is untouched: Is still matches its sentinel, accessors still return its fields, and the original error remains reachable through Unwrap, so logging and metrics observe the real failure before the projection hides it.

It is the mechanism behind strict throws assertion: a generated wrapper that catches an undeclared contract identity leaving a method does not rewrite the error — classification stays observable in-process — it marks the error, and the single disclosure gate does the rest.

func Unwrap

func Unwrap(err error) error

Unwrap returns the result of calling the Unwrap method on err, if err's type contains an Unwrap method returning error. Otherwise, Unwrap returns nil.

Types

type Error

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

Error is a Forge error.

It is a plain Go value with no generated machinery embedded in it. Each transport owns its own projection — transport/http the Problem document, transport/grpc the status and its details — leaving this package free of protocol dependencies and free to evolve independently.

An Error is immutable once constructed. Every method that appears to modify one returns a copy, so a package-level sentinel is safe to share across goroutines.

Every method tolerates a nil receiver: accessors return zero values, and deriving methods treat nil as the zero-value error, so a typed-nil *Error never panics.

func FromError

func FromError(err error) *Error

FromError converts any error into an *Error.

It returns nil only for a nil error. An error that did not originate from this package is reported as KindUnknown, wrapped so its own type remains reachable with errors.As.

func FromPublic

func FromPublic(p Public) *Error

FromPublic reconstructs an error from data received over the wire.

It is the inverse of PublicOf: one type describes what may leave a process and what arrives from one, because those are the same set of facts seen from two sides.

The result is marked remote: it describes a failure in another process, so it carries no cause and errors.As will not reach a local type through it.

An identity is accepted only as a complete pair. A domain without a reason, or a reason without a domain, is not an identity a sentinel can be matched against, and keeping half of one would let unrelated failures compare equal.

func MustDefine

func MustDefine(kind Kind, domain, reason string) *Error

MustDefine returns an immutable sentinel error, panicking when the declaration is invalid. It is the constructor used by generated code; hand-written code normally declares errors in Protobuf instead, or uses Of for failures that never leave the process.

It panics rather than returning an error because a sentinel is package state built during initialization: there is no caller yet to handle a failure, and an invalid declaration is a programming error rather than a runtime condition. Failing at init surfaces the mistake on the first run, instead of letting a sentinel reach the wire with an identity that Error.Is cannot match anything against. The same reasoning already governs encoding.RegisterCodec.

func Of

func Of(kind Kind) *Error

Of returns an error of the given Kind. Use it for failures that are internal to a process and therefore need no Protobuf declaration; contract errors should be declared in Protobuf and generated.

func (*Error) Domain

func (e *Error) Domain() string

Domain returns the namespace that owns the reason, normally the Protobuf package that declared it.

func (*Error) Error

func (e *Error) Error() string

Error implements the error interface.

The formatted value includes the local cause chain, as Go wrapping errors conventionally do. Error.Message returns only the public-facing message; transports serialize that field and never this formatted string.

func (*Error) Is

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

Is reports whether target identifies the same failure as e.

Identity is the complete domain and reason pair: two errors match exactly when both carry the same non-empty domain and the same non-empty reason. An error missing either half has no cross-instance identity and matches only itself — the standard library compares values before calling this method — so two anonymous errors of one Kind never match each other. Classifying by Kind is KindOf's job.

Kind and message deliberately do not participate. The message is descriptive and may be interpolated per occurrence, and the Kind is a classification a transport boundary may recompute in transit, so neither can be allowed to separate two reports of the same failure.

func (*Error) IsRemote

func (e *Error) IsRemote() bool

IsRemote reports whether this error was reconstructed from the wire rather than produced in this process.

func (*Error) Kind

func (e *Error) Kind() Kind

Kind returns the error's category.

func (*Error) Message

func (e *Error) Message() string

Message returns the human-readable description.

func (*Error) Meta

func (e *Error) Meta(key, value string) *Error

Meta returns a copy of e with one metadata entry set.

func (*Error) Metadata

func (e *Error) Metadata() map[string]string

Metadata returns a copy of the error's metadata. The copy keeps a caller from mutating a shared sentinel.

func (*Error) Msg

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

Msg returns a copy of e with the given message.

func (*Error) Msgf

func (e *Error) Msgf(format string, a ...any) *Error

Msgf returns a copy of e with a formatted message.

func (*Error) Reason

func (e *Error) Reason() string

Reason returns the stable machine-readable identifier for this failure.

func (*Error) TraceID

func (e *Error) TraceID() string

TraceID returns the trace this error was produced in, or the empty string. It is the supported way to correlate a failure across process boundaries.

func (*Error) Unwrap

func (e *Error) Unwrap() error

Unwrap returns the underlying cause, or nil if there is none.

An error received from another process never has a cause: the chain does not cross a boundary. Correlate such a failure by its trace ID, and read the unredacted detail in the producing service's logs.

func (*Error) Violations

func (e *Error) Violations() []Violation

Violations returns the per-field failures of an aggregate error.

func (*Error) WithDomain

func (e *Error) WithDomain(domain string) *Error

WithDomain returns a copy of e with the given domain.

func (*Error) WithMetadata

func (e *Error) WithMetadata(md map[string]string) *Error

WithMetadata returns a copy of e with the given metadata merged in.

func (*Error) WithReason

func (e *Error) WithReason(reason string) *Error

WithReason returns a copy of e with the given reason. It is intended for errors that are internal to a process; contract errors carry a reason from their Protobuf declaration.

func (*Error) WithTraceID

func (e *Error) WithTraceID(id string) *Error

WithTraceID returns a copy of e carrying the given trace identifier.

func (*Error) Wrap

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

Wrap returns a copy of e recording cause as the underlying error.

Prefer it over folding cause.Error() into the message: the message is for humans, the cause is for errors.Is and errors.As. Wrapping a nil cause is a no-op, so a call site need not branch on it.

type Kind

type Kind uint8

Kind classifies a failure independently of any transport. It is the single source of truth for an error's category: transports project a Kind onto their own status vocabulary, and never the reverse.

The set is closed and deliberately small. It mirrors the gRPC canonical codes because that vocabulary is the narrower of the two Forge speaks; projecting one way from the narrow space keeps every projection total and lossless.

A Kind carries no transport vocabulary of its own. Each transport owns its projection, so that this package stays free of protocol dependencies: transport/http maps a Kind to an HTTP status, and transport/grpc to a gRPC code.

const (
	// KindUnknown is an unclassified failure.
	KindUnknown Kind = iota
	// KindInvalidArgument means the caller supplied a malformed argument.
	KindInvalidArgument
	// KindFailedPrecondition means the system is not in the state the call requires.
	KindFailedPrecondition
	// KindOutOfRange means an argument was outside the valid range.
	KindOutOfRange
	// KindUnauthenticated means the caller could not be identified.
	KindUnauthenticated
	// KindPermissionDenied means the caller is known but not allowed.
	KindPermissionDenied
	// KindNotFound means the requested entity does not exist.
	KindNotFound
	// KindAlreadyExists means the entity the caller tried to create is present.
	KindAlreadyExists
	// KindConflict means a concurrent change prevented the call from completing.
	KindConflict
	// KindResourceExhausted means a quota or rate limit was reached.
	KindResourceExhausted
	// KindCanceled means the caller went away before the call completed.
	KindCanceled
	// KindDeadlineExceeded means the call outlived its deadline.
	KindDeadlineExceeded
	// KindUnavailable means a dependency is temporarily unreachable.
	KindUnavailable
	// KindUnimplemented means the operation is not supported.
	KindUnimplemented
	// KindInternal means an invariant was broken. It denotes a bug.
	KindInternal
	// KindDataLoss means data was lost or irrecoverably corrupted.
	KindDataLoss
)

The Kind vocabulary. KindUnknown is the zero value so that an unset Kind classifies as unknown rather than silently claiming a specific meaning.

func KindOf

func KindOf(err error) Kind

KindOf returns the Kind of any error.

It returns KindUnknown for a nil error and for any error that did not originate from this package. Recognizing a foreign error — a gRPC status, an HTTP response — belongs to the transport that received it, which converts before the error reaches application code. It never panics, including for a typed-nil *Error.

func ParseKind

func ParseKind(name string) (Kind, bool)

ParseKind returns the Kind for a stable wire name, and reports whether the name was recognized. An unrecognized name yields KindUnknown.

func (Kind) String

func (k Kind) String() string

String returns the stable wire name of the Kind.

type Public

type Public struct {
	Kind       Kind
	Domain     string
	Reason     string
	Message    string
	Metadata   map[string]string
	TraceID    string
	Violations []Violation
}

Public is the data an error may disclose outside its process.

It is the only input a transport accepts, so what crosses a boundary is decided by construction rather than by inspection. Everything a caller declared — the message, the metadata, the violations — is here; the cause chain and any wrapped Go value are not, because there is no way to declare those safe.

The alternative Forge used to ship read the Kind and guessed. That model could not observe what it needed to: a KindNotFound message may name a tenant, a violation description may quote a driver's error, and neither is visible to a rule written in terms of Kind. Only the caller who wrote the field knows whether it is public, and calling Msg, Meta, WithMetadata, or adding a Violation is how they say so.

func PublicOf

func PublicOf(err error) Public

PublicOf returns the disclosable data of any error.

The result owns its maps and slices, so a transport cannot mutate the error it came from, and never contains a cause, a formatted error string, or a wrapped value. An error that did not originate from this package discloses only KindUnknown: its text was written for an operator, not for a caller, and a transport supplies its own generic message instead.

Disclosure is gated on declaration. An error speaks for a service's contract only when its identity was declared through MustDefine — which is how generated *_errors.pb.go files and deliberate framework sentinels come into being. A locally produced error whose identity was never declared — an Of product, or an ad-hoc WithDomain/WithReason pair — projects as an internal failure carrying only its trace ID: its Kind, reason, message, metadata, and violations were assembled for in-process use, and letting them cross would both leak internal taxonomy and freeze accidental reasons into public API. The original classification is not lost to the operator: logging and metrics observe the error itself, before projection.

A remote error is exempt: its data arrived over the wire from a peer that already chose to disclose it, so passing it on discloses nothing new. That keeps proxied statuses and health-check semantics intact.

type Violation

type Violation struct {
	// Field identifies what failed, as a path into the request message
	// (for example "user.email" or "items[2].quantity").
	Field string
	// Description explains the failure in terms a caller can act on.
	Description string
}

Violation is a single field-level failure within an aggregate error.

type Violations

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

Violations collects field-level failures so that a validation pass can report everything it found rather than only the first problem.

The zero value is ready to use:

var v errors.Violations
v.Add("email", "malformed")
v.Add("age", "must be positive")
if !v.Empty() {
	return v.Err(errors.KindInvalidArgument)
}

Every entry survives the RPC boundary. Note that Join does not aggregate in this sense: a joined error can only project one status onto the wire, so the others would be dropped there. Aggregation is explicit for that reason.

func (*Violations) Add

func (v *Violations) Add(field, description string)

Add records a field-level failure.

func (*Violations) Addf

func (v *Violations) Addf(field, format string, a ...any)

Addf records a field-level failure with a formatted description.

func (*Violations) All

func (v *Violations) All() []Violation

All returns a copy of the recorded violations.

func (*Violations) Empty

func (v *Violations) Empty() bool

Empty reports whether no violations were recorded.

func (*Violations) Err

func (v *Violations) Err(kind Kind) error

Err returns an aggregate error carrying every recorded violation, or nil when none were recorded. Returning nil for an empty set lets a caller return the result unconditionally.

func (*Violations) Len

func (v *Violations) Len() int

Len returns the number of recorded violations.

Jump to

Keyboard shortcuts

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