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 ¶
- Constants
- Variables
- func As(err error, target any) bool
- func DomainOf(err error) string
- func Is(err, target error) bool
- func IsContract(domain, reason string) bool
- func IsUndisclosed(err error) bool
- func Join(errs ...error) error
- func ReasonOf(err error) string
- func Undisclose(err error) error
- func Unwrap(err error) error
- type Error
- func (e *Error) Domain() string
- func (e *Error) Error() string
- func (e *Error) Is(target error) bool
- func (e *Error) IsRemote() bool
- func (e *Error) Kind() Kind
- func (e *Error) Message() string
- func (e *Error) Meta(key, value string) *Error
- func (e *Error) Metadata() map[string]string
- func (e *Error) Msg(message string) *Error
- func (e *Error) Msgf(format string, a ...any) *Error
- func (e *Error) Reason() string
- func (e *Error) TraceID() string
- func (e *Error) Unwrap() error
- func (e *Error) Violations() []Violation
- func (e *Error) WithDomain(domain string) *Error
- func (e *Error) WithMetadata(md map[string]string) *Error
- func (e *Error) WithReason(reason string) *Error
- func (e *Error) WithTraceID(id string) *Error
- func (e *Error) Wrap(cause error) *Error
- type Kind
- type Public
- type Violation
- type Violations
Examples ¶
Constants ¶
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.
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 ¶
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 ¶
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 Is ¶
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
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
IsUndisclosed reports whether err carries the Undisclose marker.
func Join ¶
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 Undisclose ¶ added in v0.0.3
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.
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
Domain returns the namespace that owns the reason, normally the Protobuf package that declared it.
func (*Error) Error ¶
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 ¶
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 ¶
IsRemote reports whether this error was reconstructed from the wire rather than produced in this process.
func (*Error) Metadata ¶
Metadata returns a copy of the error's metadata. The copy keeps a caller from mutating a shared sentinel.
func (*Error) TraceID ¶
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 ¶
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 ¶
Violations returns the per-field failures of an aggregate error.
func (*Error) WithDomain ¶
WithDomain returns a copy of e with the given domain.
func (*Error) WithMetadata ¶
WithMetadata returns a copy of e with the given metadata merged in.
func (*Error) WithReason ¶
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 ¶
WithTraceID returns a copy of e carrying the given trace identifier.
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 // 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 ¶
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.
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 ¶
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.