Documentation
¶
Overview ¶
Package errors is a drop-in replacement for the standard errors package that provides a consistent API and adds capabilities to make error messages more informative.
Features
- Add structured context to errors in addition to plain text.
- Attach the location where an error was created/handled (file:line).
- Define constant error values via a dedicated type.
Rationale ¶
The standard fmt.Errorf becomes insufficient when you need to attach rich context describing the conditions that led to an error. Once you add more than a couple of values, the quality of the error message tends to suffer:
- The separation between general and specific information is lost; everything gets mixed together, which makes the message harder to read.
A common workaround is to log at every point where an error is observed, but this has drawbacks:
- You have to pass a logger into places that otherwise wouldn't need it.
- Logs become bloated, as the same error (with different annotations) is logged multiple times.
- Methodologically, each log line combines error + context + system-wide metadata, which hurts the signal-to-noise ratio.
This library lets you attach context to the error itself. As a result, logging preserves the separation of general and specific information without overloading the logs. The final output quality can be higher than with logging at every stage (depending on the logger implementation).
Index ¶
- func As(err error, target any) bool
- func AsType[T error](err error) (T, bool)
- func DoNotInsertLocations()
- func InsertLocations()
- func Is(err, target error) bool
- func Join(errs ...error) error
- func Log(err error) slog.Attr
- type Const
- type Context
- func (b *Context) Any(name string, value any) *Context
- func (b *Context) Bool(name string, value bool) *Context
- func (b *Context) Bytes(name string, value []byte) *Context
- func (b *Context) Flt32(name string, value float32) *Context
- func (b *Context) Flt64(name string, value float64) *Context
- func (b *Context) Int(name string, value int) *Context
- func (b *Context) Int8(name string, value int8) *Context
- func (b *Context) Int16(name string, value int16) *Context
- func (b *Context) Int32(name string, value int32) *Context
- func (b *Context) Int64(name string, value int64) *Context
- func (b *Context) Stg(name string, value fmt.Stringer) *Context
- func (b *Context) Str(name string, value string) *Context
- func (b *Context) Strs(name string, value []string) *Context
- func (b *Context) Type(name string, typ any) *Context
- func (b *Context) Uint(name string, value uint) *Context
- func (b *Context) Uint8(name string, value uint8) *Context
- func (b *Context) Uint16(name string, value uint16) *Context
- func (b *Context) Uint32(name string, value uint32) *Context
- func (b *Context) Uint64(name string, value uint64) *Context
- func (b *Context) WithCtx(ctx *Context) *Context
- type Error
- func (e *Error) Any(name string, value any) *Error
- func (e *Error) As(target any) bool
- func (e *Error) Bool(name string, value bool) *Error
- func (e *Error) Bytes(name string, value []byte) *Error
- func (e *Error) Error() string
- func (e *Error) Flt32(name string, value float32) *Error
- func (e *Error) Flt64(name string, value float64) *Error
- func (e *Error) Int(name string, value int) *Error
- func (e *Error) Int8(name string, value int8) *Error
- func (e *Error) Int16(name string, value int16) *Error
- func (e *Error) Int32(name string, value int32) *Error
- func (e *Error) Int64(name string, value int64) *Error
- func (e *Error) Is(err error) bool
- func (e *Error) Pfx(prefix string) *Error
- func (e *Error) Stg(name string, value fmt.Stringer) *Error
- func (e *Error) Str(name string, value string) *Error
- func (e *Error) Strs(name string, value []string) *Error
- func (e *Error) Type(name string, typ any) *Error
- func (e *Error) Uint(name string, value uint) *Error
- func (e *Error) Uint8(name string, value uint8) *Error
- func (e *Error) Uint16(name string, value uint16) *Error
- func (e *Error) Uint32(name string, value uint32) *Error
- func (e *Error) Uint64(name string, value uint64) *Error
- func (e *Error) Unwrap() error
- func (e *Error) WithCtx(ctx *Context) *Error
- type ErrorChainLinkContext
- type ErrorChainLinkDescriptor
- type ErrorChainLinkNew
- type ErrorChainLinkWrap
- type ErrorContextConsumer
- type ErrorContextDeliverer
- type SLogERrorContextFlatHandler
- type SLogErrorContextGrouppedHandler
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func As ¶
As is a wrapper for errors.As from the standard library.
Example ¶
package main
import (
"fmt"
"github.com/sirkon/errors"
)
func main() {
err := fmt.Errorf("read data: %w", errors.New("root error"))
var e *errors.Error
if errors.As(err, &e) {
fmt.Println(e.Error())
}
var ae errors.Const
if errors.As(err, &ae) {
fmt.Println(e.Error())
}
}
Output: root error
func AsType ¶ added in v1.0.0
AsType is a type-safe generic version of As, allowing usage without pre-declaring a variable.
Example ¶
package main
import (
"fmt"
"github.com/sirkon/errors"
)
func main() {
err := fmt.Errorf("read data: %w", errors.New("root error"))
if v, ok := errors.AsType[*errors.Error](err); ok {
fmt.Println(v)
}
if _, ok := errors.AsType[errors.Const](err); ok {
fmt.Println("must not be here with", err)
}
}
Output: root error
func DoNotInsertLocations ¶ added in v1.0.0
func DoNotInsertLocations()
DoNotInsertLocations disables the insertion of error handling positions. This is the default mode.
func InsertLocations ¶ added in v1.0.0
func InsertLocations()
InsertLocations enables the insertion of error handling positions.
WARNING!
Calculating positions is a very expensive operation, so it is highly discouraged to enable position capturing in production. Enable only during local debugging.
The recommended usage pattern is enabling when using "development" logging mode, with colored output and formatting intended for reading.
func Is ¶
Is is a wrapper for errors.Is from the standard library.
Example ¶
package main
import (
"fmt"
"io"
"github.com/sirkon/errors"
)
func main() {
err := errors.Wrap(io.ErrNoProgress, "read data")
fmt.Println(errors.Is(err, io.ErrNoProgress))
fmt.Println(errors.Is(
errors.Wrap(errors.New("error"), "wrapped"),
io.EOF,
))
}
Output: true false
func Join ¶ added in v0.6.0
Join is a wrapper for errors.Join from the standard library.
func Log ¶ added in v1.0.0
Log is used to log an error as a field with the key "err" in slog loggers. For some reason, despite the prevalence of this pattern in other structured loggers, similar functionality was not included in slog.
Usage:
logger.Error("failed to get user data",
slog.Str("actor-id", actor.ID),
slog.Str("user-id", user.ID),
errors.Log(err),
)
WARNING the purpose is questionable, may be to remove it?
Types ¶
type Const ¶ added in v0.2.0
type Const string
Const is a type implementing the error interface and capable of being a constant.
func (Const) Is ¶ added in v0.2.0
Is for errors.Is.
Example ¶
package main
import (
"fmt"
"github.com/sirkon/errors"
)
func main() {
err := errors.Wrap(errors.Const("error"), "message")
fmt.Println(errors.Is(err, errors.Const("error")))
fmt.Println(errors.Is(err, errors.Const("another error")))
fmt.Println(errors.Is(errors.New("error"), errors.Const("error")))
}
Output: true false false
type Context ¶ added in v0.1.0
type Context struct {
// contains filtered or unexported fields
}
Context is an entity for implementing context composition. Using this object implies mutability; for a reuse approach, use Context.WithCtx.
func Ctx ¶ added in v1.0.0
func Ctx() *Context
Ctx creates an empty Context.
Example ¶
// Basic demo to show how context is stored and extracted.
errctx := errors.Ctx().
Bool("true", true).
Int("int", 12).
Int8("i8", math.MinInt8).
Int16("i16", math.MinInt16).
Int32("i32", math.MinInt32).
Int64("i64", math.MinInt64).
Uint("u", math.MaxUint).
Uint8("u8", math.MaxUint8).
Uint16("u16", math.MaxUint16).
Uint32("u32", math.MaxUint32).
Uint64("u64", math.MaxUint64).
Flt32("e", math.E).
Flt64("pi", math.Pi).
Str("str", "hello world").
Stg("stringer", testStringer{}).
Strs("strlist", strings.Split("1 2 3", " ")).
Bytes("bytes-printable", []byte("123")).
Bytes("bytes-raw", []byte{1, 2, 3}).
Type("type", new(bytes.Buffer)).
Any("map", map[int]int{1: 3, 2: 2, 3: 1})
err := errors.New("error").
Int("code", 404).
Pfx("ctx").
WithCtx(errctx)
LogError(err)
Output: error message: error - code: 404 - ctx-true: true - ctx-int: 12 - ctx-i8: -128 - ctx-i16: -32768 - ctx-i32: -2147483648 - ctx-i64: -9223372036854775808 - ctx-u: 18446744073709551615 - ctx-u8: 255 - ctx-u16: 65535 - ctx-u32: 4294967295 - ctx-u64: 18446744073709551615 - ctx-e: 2.7182817 - ctx-pi: 3.141592653589793 - ctx-str: hello world - ctx-stringer: test stringer - ctx-strlist: [1 2 3] - ctx-bytes-printable: 123 - ctx-bytes-raw: [1 2 3] - ctx-type: *bytes.Buffer - ctx-map: map[1:3 2:2 3:1]
func CtxFrom ¶ added in v1.0.0
CtxFrom creates a new context by copying values from an existing one.
func (*Context) Bool ¶ added in v1.0.0
Bool adds a named boolean value (bool) to the context builder.
func (*Context) Bytes ¶ added in v1.0.0
Bytes adds a named slice of bytes value to the context builder.
Attention:
This operation can be computationally heavy, as it evaluates whether the sequence can be represented as a string, after which it is saved in either string or object format.
func (*Context) Flt32 ¶ added in v1.0.0
Flt32 adds a named floating-point value (float32) to the context builder.
func (*Context) Flt64 ¶ added in v1.0.0
Flt64 adds a named floating-point value (float64) to the context builder.
func (*Context) Int8 ¶ added in v1.0.0
Int8 adds a named integer value (int8) to the context builder.
func (*Context) Int16 ¶ added in v1.0.0
Int16 adds a named integer value (int16) to the context builder.
func (*Context) Int32 ¶ added in v1.0.0
Int32 adds a named integer value (int32) to the context builder.
func (*Context) Int64 ¶ added in v1.0.0
Int64 adds a named integer value (int64) to the context builder.
func (*Context) Stg ¶ added in v1.0.0
Stg adds a named value implementing fmt.Stringer to the context builder.
func (*Context) Strs ¶ added in v1.0.0
Strs adds a named slice of strings value to the context builder.
func (*Context) Uint ¶ added in v1.0.0
Uint adds a named unsigned integer value (uint) to the context builder.
func (*Context) Uint8 ¶ added in v1.0.0
Uint8 adds a named unsigned integer value (uint8) to the context builder.
func (*Context) Uint16 ¶ added in v1.0.0
Uint16 adds a named unsigned integer value (uint16) to the context builder.
func (*Context) Uint32 ¶ added in v1.0.0
Uint32 adds a named unsigned integer value (uint32) to the context builder.
func (*Context) Uint64 ¶ added in v1.0.0
Uint64 adds a named unsigned integer value (uint64) to the context builder.
func (*Context) WithCtx ¶ added in v1.0.0
WithCtx copies values from the given context into the current one.
This method supports logic where multiple contexts can be used, which may have common parts but differ in others. Something like:
ctx1 := errors.Ctx().Int("code", 404)
ctx2 := ctx1.Str("msg", "not found")
ctx3 := ctx1.Bool("retried", isRetried)
Will not work. Here ctx1, ctx2, and ctx3 will be pointers to the same object and, accordingly, will yield the same context.
See the example for detailed usage.
Example ¶
// Why simply using a variable for context won't work.
ctxBase := errors.Ctx().Int("code", 404)
ctxMsg := ctxBase.Str("msg", "not found")
ctxBool := ctxBase.Bool("found", false)
LogError(errors.New("error-code").WithCtx(ctxBase))
LogError(errors.New("error-msg").WithCtx(ctxMsg))
LogError(errors.New("error-bool").WithCtx(ctxBool))
fmt.Println()
fmt.Println("...reuse context properly...")
ctxBase = errors.Ctx().Int("code", 403)
ctxMsg = errors.CtxFrom(ctxBase).Str("msg", "forbidden")
ctxBool = errors.CtxFrom(ctxBase).Bool("allowed", false)
LogError(errors.New("error-code").WithCtx(ctxBase))
LogError(errors.New("error-msg").WithCtx(ctxMsg))
LogError(errors.New("error-bool").WithCtx(ctxBool))
Output: error message: error-code - code: 404 - msg: not found - found: false error message: error-msg - code: 404 - msg: not found - found: false error message: error-bool - code: 404 - msg: not found - found: false ...reuse context properly... error message: error-code - code: 403 error message: error-msg - code: 403 - msg: forbidden error message: error-bool - code: 403 - allowed: false
type Error ¶ added in v0.2.0
type Error struct {
// contains filtered or unexported fields
}
Error is an error implementation with structured context support.
func Just ¶ added in v0.5.0
Just is for when annotation is not needed, but structured context is. This function solves that problem by returning an error with the text from the given one and allowing to add structured values.
Example ¶
err := errors.Just(io.EOF).
Str("name", "value").
Int("value", 1)
LogError(err)
Output: error message: EOF - name: value - value: 1
func New ¶
New creates a new error with the given message.
Example ¶
package main
import (
"fmt"
"github.com/sirkon/errors"
)
func main() {
fmt.Println(errors.New("error example"))
}
Output: error example
func Newf ¶
Newf creates a new error with the given formatted message.
Example ¶
package main
import (
"fmt"
"github.com/sirkon/errors"
)
func main() {
fmt.Println(errors.Newf("error %s", "example"))
}
Output: error example
func Wrap ¶
Wrap annotates an error with a text message.
Example ¶
package main
import (
"fmt"
"github.com/sirkon/errors"
)
func main() {
fmt.Println(errors.Wrap(errors.Const("example"), "error"))
}
Output: error: example
func Wrapf ¶
Wrapf annotates an error with a formatted text message.
Example ¶
package main
import (
"fmt"
"github.com/sirkon/errors"
)
func main() {
fmt.Println(errors.Wrapf(errors.Const("example"), "formatted error"))
}
Output: formatted error: example
func (*Error) Bytes ¶ added in v1.0.0
Bytes adds a named slice of bytes value to the context.
Attention:
This operation can be computationally heavy, as it evaluates whether the sequence can be represented as a string, after which it is saved in either string or object format.
func (*Error) Flt32 ¶ added in v1.0.0
Flt32 adds a named floating-point value (float32) to the context.
func (*Error) Flt64 ¶ added in v1.0.0
Flt64 adds a named floating-point value (float64) to the context.
func (*Error) Pfx ¶ added in v0.3.0
Pfx sets (or replaces) the prefix that will be prepended to subsequent context field names.
func (*Error) Stg ¶ added in v0.2.0
Stg adds a named value implementing fmt.Stringer to the context.
func (*Error) Uint ¶ added in v0.2.0
Uint adds a named unsigned integer value (uint) to the context.
func (*Error) Uint8 ¶ added in v0.2.0
Uint8 adds a named unsigned integer value (uint8) to the context.
func (*Error) Uint16 ¶ added in v0.2.0
Uint16 adds a named unsigned integer value (uint16) to the context.
func (*Error) Uint32 ¶ added in v0.2.0
Uint32 adds a named unsigned integer value (uint32) to the context.
func (*Error) Uint64 ¶ added in v0.2.0
Uint64 adds a named unsigned integer value (uint64) to the context.
type ErrorChainLinkContext ¶ added in v1.0.0
type ErrorChainLinkContext struct{}
ErrorChainLinkContext is given to links where only structured context was added via Just.
type ErrorChainLinkDescriptor ¶ added in v1.0.0
type ErrorChainLinkDescriptor interface {
// contains filtered or unexported methods
}
ErrorChainLinkDescriptor describes a link in the processing chain. Implementations of this interface are used to describe the file:line locations in the error chain.
type ErrorChainLinkNew ¶ added in v1.0.0
type ErrorChainLinkNew string
ErrorChainLinkNew is given to links related to the creation of a new error via New/Newf.
type ErrorChainLinkWrap ¶ added in v1.0.0
type ErrorChainLinkWrap string
ErrorChainLinkWrap is given to chain links obtained via Wrap/Wrapf.
type ErrorContextConsumer ¶ added in v0.2.0
type ErrorContextConsumer interface {
NextLink()
Bool(name string, value bool)
Int(name string, value int)
Int8(name string, value int8)
Int16(name string, value int16)
Int32(name string, value int32)
Int64(name string, value int64)
Uint(name string, value uint)
Uint8(name string, value uint8)
Uint16(name string, value uint16)
Uint32(name string, value uint32)
Uint64(name string, value uint64)
Flt32(name string, value float32)
Flt64(name string, value float64)
Str(name string, value string)
Any(name string, value any)
SetLinkInfo(loc token.Position, descr ErrorChainLinkDescriptor)
}
ErrorContextConsumer is the contract that must be implemented by the logging side to receive the structured values stored in the error.
type ErrorContextDeliverer ¶ added in v0.2.0
type ErrorContextDeliverer interface {
Deliver(cons ErrorContextConsumer)
Error() string
}
ErrorContextDeliverer is what is returned by the GetContextDeliverer function. Example of how to work with slog.
type slogConsumer struct{
fields []any
}
func (c *slogConsumer) Int(name, value int) {
c.fields = append(c.fields, slog.Int(name, value))
}
// And so on for the rest of the methods to implement errors.ErrorContextConsumer.
// Log logs at the Info level.
func Log(msg string, fields []slog.Attr) {
var attrs []any
for _, field := range fields {
// Save all original fields.
attrs = append(attrs, field)
// Need to unwrap errors. For this, we look for fields that contain them.
fv := field.Value.Any()
e, ok := fv.(error)
if !ok {
continue
}
// v contains an error. Try to get context from it.
dlr := errors.GetContextDeliverer(e)
if dlr == nil {
// This is not our error.
continue
}
// Get the context and add the extracted fields.
var errCtx slogConsumer{}
dlr.Deliver(&errCtx)
attrs = append(attrs, errCtx.fields...)
}
slog.Info(msg, attrs...)
}
A working example can be found in ./internal/example/main.go.
func GetContextDeliverer ¶ added in v0.2.0
func GetContextDeliverer(err error) ErrorContextDeliverer
GetContextDeliverer returns the structured-context deliverer for the given error.
Example ¶
package main
import (
"bytes"
"fmt"
"math"
"strconv"
"github.com/sirkon/errors"
)
func main() {
// Handle the case of an error without structured context.
if errors.GetContextDeliverer(errors.Const("error")) != nil {
fmt.Println("must not be here")
}
// Now the main case where we need to show values from the context.
// Here we add the prefix "error" – call Pfx("error") – and add
// a bunch of values of various types.
err := errors.New("error").Pfx("").Pfx("context").
Bool("b", true).
Int("i", -1).
Int8("i8", math.MinInt8).
Int16("i16", math.MinInt16).
Int32("i32", math.MinInt32).
Int64("i64", math.MinInt64).
Uint("u", 1).
Uint8("u8", math.MaxUint8).
Uint16("u16", math.MaxUint16).
Uint32("u32", math.MaxUint32).
Uint64("u64", math.MaxUint64).
Flt32("f32", math.MaxFloat32).
Flt64("f64", math.MaxFloat64).
Str("string", "str").
Strs("strings", []string{"1", "2", "3"}).
Stg("stringer", testStringer{}).
Any("object", map[string]int{
"key": 12,
}).
Bytes("bytes", []byte("123")).Bytes("bytes-raw", []byte{1, 2, 3}).
Type("type-name", bytes.NewBuffer(nil))
// Add a bit more context, this time without a prefix.
err = errors.Wrap(err, "wrapping context").Str("wrapped", "value")
// Print the error text.
fmt.Println("error message:", strconv.Quote(err.Error()))
// Get the context deliverer and feed it our ready implementation
// of the context receiver, then output the accumulated data to STDOUT with a bit of formatting.
cons := &testConsumer{}
d := errors.GetContextDeliverer(err)
d.Deliver(cons)
for _, ctx := range cons.ctx {
fmt.Println(" - "+ctx.name+":", ctx.value)
}
}
type testStringer struct{}
func (testStringer) String() string {
return "test stringer"
}
func init() {
// Enable to slightly improve coverage.
errors.InsertLocations()
}
Output: error message: "wrapping context: error" - wrapped: value - context-b: true - context-i: -1 - context-i8: -128 - context-i16: -32768 - context-i32: -2147483648 - context-i64: -9223372036854775808 - context-u: 1 - context-u8: 255 - context-u16: 65535 - context-u32: 4294967295 - context-u64: 18446744073709551615 - context-f32: 3.4028235e+38 - context-f64: 1.7976931348623157e+308 - context-string: str - context-strings: [1 2 3] - context-stringer: test stringer - context-object: map[key:12] - context-bytes: 123 - context-bytes-raw: [1 2 3] - context-type-name: *bytes.Buffer
type SLogERrorContextFlatHandler ¶ added in v1.1.0
SLogERrorContextFlatHandler to handle these errors with slog.Logger splitting errors by processing stages.
func NewSLogErrorContextFlatHandler ¶ added in v1.1.0
func NewSLogErrorContextFlatHandler(handler slog.Handler) *SLogERrorContextFlatHandler
type SLogErrorContextGrouppedHandler ¶ added in v1.1.0
SLogErrorContextGrouppedHandler to handle these errors with slog.Logger splitting errors by processing stages.
func NewSLogErrorContextGrouppedHandler ¶ added in v1.1.0
func NewSLogErrorContextGrouppedHandler(handler slog.Handler) *SLogErrorContextGrouppedHandler