Documentation
¶
Overview ¶
Package errs is the single error vocabulary of an inceptools application.
A service layer never knows about HTTP. It returns an *errs.Error, and the transport decides what that becomes on the wire. There is no hierarchy of error types to learn — one concrete type with a machine-readable Code, a transport-neutral Kind, a caller-safe Message and an optional internal Cause that is never serialized.
The rule that makes this worth having: Message is the only field that reaches a client. Anything you must not leak — a driver string, an upstream reply, a query — goes in Cause, where it reaches the log and nothing else.
Example ¶
A service decides what a failure means without importing net/http. The transport reads the Kind and picks a status.
package main
import (
"fmt"
"github.com/IncepTools/go/src/core/errs"
)
func main() {
err := errs.Conflict("a product with the SKU %q already exists", "DESK-01").
WithCode("SKU_TAKEN")
fmt.Println(errs.KindOf(err))
fmt.Println(errs.CodeOf(err))
fmt.Println(err)
}
Output: conflict SKU_TAKEN a product with the SKU "DESK-01" already exists
Index ¶
- func CodeOf(err error) string
- func Is(err error, kind Kind) bool
- type Detail
- type Error
- func Conflict(format string, args ...any) *Error
- func Exhausted(format string, args ...any) *Error
- func Forbidden(format string, args ...any) *Error
- func From(err error) *Error
- func Internal(format string, args ...any) *Error
- func Invalid(format string, args ...any) *Error
- func NotFound(format string, args ...any) *Error
- func NotSupported(format string, args ...any) *Error
- func PaymentRequired(format string, args ...any) *Error
- func Timeout(format string, args ...any) *Error
- func Unauthorized(format string, args ...any) *Error
- func Unavailable(format string, args ...any) *Error
- func Wrap(kind Kind, message string, err error) *Error
- type Kind
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func Is ¶
Is reports whether err is (or wraps) an *Error of the given kind.
Example ¶
The chain survives fmt.Errorf wrapping, so a caller several layers up can still branch on the kind.
package main
import (
"fmt"
"github.com/IncepTools/go/src/core/errs"
)
func main() {
inner := errs.NotFound("product not found").WithCode("NOT_FOUND")
outer := fmt.Errorf("loading the product page: %w", inner)
fmt.Println(errs.Is(outer, errs.KindNotFound))
fmt.Println(errs.CodeOf(outer))
}
Output: true NOT_FOUND
Types ¶
type Detail ¶
type Detail struct {
Field string `json:"field"`
Message string `json:"message"`
Rule string `json:"rule,omitempty"`
}
Detail is one field-scoped explanation attached to an error. Validation failures are the common producer, but any error may carry them.
type Error ¶
type Error struct {
// Kind decides the transport status. Always set; zero value is treated as
// KindInternal.
Kind Kind
// Code is a stable machine identifier a client may branch on, such as
// "SKU_ALREADY_TAKEN". Optional, but strongly preferred over parsing Message.
Code string
// Message is caller-safe prose. It is the ONLY field that reaches a client.
Message string
// Details carry per-field explanations.
Details []Detail
// Cause is the internal error. Logged, never serialized.
Cause error
// Meta carries structured context for the log — ids, names, counts. Like
// Cause, it does not reach the client.
Meta map[string]any
}
Error is the one error type inceptools code returns across a layer boundary.
func From ¶
From normalises any error into an *Error. An error that is already one (or wraps one) is returned as-is; anything else becomes KindInternal with the original as Cause, so it is logged but not leaked.
Example ¶
An error nobody classified is treated as internal, so its text cannot reach a client by accident.
package main
import (
"errors"
"fmt"
"github.com/IncepTools/go/src/core/errs"
)
func main() {
plain := errors.New("dial tcp 10.1.4.7:5432: connection refused")
e := errs.From(plain)
fmt.Println(errs.KindOf(e))
fmt.Println(e.Message)
}
Output: internal internal error
func Internal ¶
Internal reports a defect. The message is NOT shown to clients by the web layer; it is replaced with a generic sentence and logged under the trace id.
func NotFound ¶
NotFound reports a resource that does not exist OR is not visible to this tenant. Returning Forbidden for the second case confirms the resource exists to a caller who should not know; prefer NotFound for both.
func NotSupported ¶
NotSupported reports something this deployment will never do.
func PaymentRequired ¶
PaymentRequired reports insufficient credit or a declined charge.
func Unauthorized ¶
Unauthorized reports a missing or unusable credential.
func Unavailable ¶
Unavailable reports a dependency that is down. Retrying may succeed.
func Wrap ¶
Wrap turns any error into an *Error of the given kind, preserving the original as Cause. A nil error wraps to nil, so it is safe to write `return errs.Wrap(errs.KindInternal, "x", err)` on a possibly-nil err.
func (*Error) WithCause ¶
WithCause returns a copy carrying an internal cause. The cause never reaches a client.
Example ¶
The cause is for the log; the message is for the client. Everything in WithCause and WithMeta stays out of the response.
package main
import (
"errors"
"fmt"
"github.com/IncepTools/go/src/core/errs"
)
func main() {
driver := errors.New("pq: SSL is not enabled on the server (host=db-7.internal)")
err := errs.Unavailable("the catalog is temporarily unavailable").
WithCode("CATALOG_UNAVAILABLE").
WithCause(driver).
WithMeta("host", "db-7.internal")
fmt.Println(err.Message) // sent to the client
fmt.Println(errors.Is(err, driver)) // still reachable for the log
}
Output: the catalog is temporarily unavailable true
func (*Error) WithDetails ¶
WithDetails returns a copy carrying field-scoped details.
Example ¶
Field-scoped details are what a form attaches to an input.
package main
import (
"fmt"
"github.com/IncepTools/go/src/core/errs"
)
func main() {
err := errs.Invalid("validation failed").
WithCode("VALIDATION_FAILED").
WithDetails(
errs.Detail{Field: "name", Message: "name is required", Rule: "required"},
errs.Detail{Field: "price", Message: "price must be 0 or more", Rule: "gte"},
)
for _, d := range err.Details {
fmt.Printf("%s: %s\n", d.Field, d.Message)
}
}
Output: name: name is required price: price must be 0 or more
type Kind ¶
type Kind string
Kind is the transport-neutral class of a failure. Adapters map it to their own status vocabulary (web/response maps it to HTTP).
const ( KindInvalid Kind = "invalid" // malformed or failed validation KindForbidden Kind = "forbidden" // authenticated, not permitted KindNotFound Kind = "not_found" // no such resource, or not visible to this tenant KindConflict Kind = "conflict" // state collision, uniqueness, concurrent update KindExhausted Kind = "exhausted" // quota or rate limit KindPayment Kind = "payment" // payment or credit required KindTimeout Kind = "timeout" // deadline exceeded KindNotSupported Kind = "not_supported" // never going to work here KindInternal Kind = "internal" // a defect; the default for an unclassified error )
The kinds. Every *Error has one; the zero value is treated as KindInternal.