errorcontext

package module
v0.1.2 Latest Latest
Warning

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

Go to latest
Published: Dec 26, 2025 License: MIT Imports: 6 Imported by: 0

README

errorcontext

Panic Handlers & Contextual Error Types for zerolog, zap Loggers & OpenTelemetry Metrics.

Why use this package?

  • ✅ Cleaner Logs: No more hunting for IDs; your context is embedded in the error.

  • ✅ Standardized Recovery: A consistent way to turn panics into trackable errors.

  • ✅ Backend Agnostic: Works seamlessly with the logging libraries you already use.

Check out the documentation and start making your Go errors more meaningful!

🔗 Docs: https://pkg.go.dev/github.com/georgepsarakis/errorcontext

📦 GitHub: https://github.com/georgepsarakis/errorcontext

Motivation

Errors are more than plain messages

Error handling should ideally occur as high in the stack as possible. A function that generates errors and must provide error context will likely:

  1. Require coupling with the logging subsystem.
  2. Require the addition of parameters which may be out-of-scope for the current function and only utilized as log record context (could be addressed with logger cloning see zap#Logger.With).
  3. Need to make a decision on whether to log the error context, with a potential overhead of redundant log records requiring correlation and increased log volumes.

However, this is often orthogonal to providing a comprehensive context within which the error was generated. For example, a database error such as a unique constraint error often requires the HTTP Request ID known at the service layer and other identifiers usually known at the query layer, to be included in the log record.

Panic Handling

Panics are exceptional errors that potentially stop goroutine and eventually program execution. The process of handling a panic is called recovery and can only be performed within a deferred function and within the stack of its origin goroutine. Unrecovered panics can cause downtime if they cause abrupt program shutdown. They can also go undetected or become difficult to inspect, as the stack trace is printed in an unstructured manner in stderr.

This package introduces a convenient wrapper that converts panics thrown by a function to error values.

Examples

zap
package main

import (
	"errors"
	"fmt"

	"go.uber.org/zap"
	"golang.org/x/sync/errgroup"

	"github.com/georgepsarakis/errorcontext"
	zaperrorcontext "github.com/georgepsarakis/errorcontext/backend/zap"
)

var ErrProcessingFailure = errors.New("processing failure")

func main() {
	cfg := zap.NewProductionConfig()
	zapLogger, err := cfg.Build()
	if err != nil {
		panic(err)
	}
	defer func() {
		_ = zapLogger.Sync()
	}()
	err = tryWithError()
	if errors.Is(err, ErrProcessingFailure) {
		zapLogger.Warn("something failed",
			zap.Dict("error_context", zaperrorcontext.AsContext(err)...),
			zap.Error(err))
	}
}

func tryWithError() error {
	return zaperrorcontext.NewError(
		ErrProcessingFailure,
		zap.String("path", "/a/b"),
		zap.Bool("enabled", true),
	)
}
Recoverer

Panics are exceptional errors that signify undefined behavior and further execution may need to be stopped. A common case is nil pointer dereference where an underlying value access is attempted while the pointer doesn't yet point to a value. Recoverer provides a structured way of handling panics and converting them to error values. Both the panic message and the stack trace are retained as error context.

In this example, zap-specific error types are used, but any error e.g. one constructed by fmt.Errorf can be used. See also DefaultErrorGenerator.

package main


import (
	"fmt"

	"go.uber.org/zap"
	"golang.org/x/sync/errgroup"

	"github.com/georgepsarakis/errorcontext"
	zaperrorcontext "github.com/georgepsarakis/errorcontext/backend/zap"
)

func main() {
	cfg := zap.NewProductionConfig()
	zapLogger, err := cfg.Build()
	if err != nil {
		panic(err)
	}
	grp := errgroup.Group{}
	recoverer := errorcontext.Recoverer[*zaperrorcontext.Error]{
		NewErrorFunc: func(p errorcontext.Panic) *zaperrorcontext.Error {
			return zaperrorcontext.FromPanic(p)
		},
	}
	grp.Go(recoverer.WrapFunc(func() error {
		panic("something bad happened")
	}))

	grp.Go(func() error {
		fmt.Println("Hello World")
		return nil
	})
	if err := grp.Wait(); err != nil {
		zapLogger.Warn("something failed",
			zap.Dict("error_context", zaperrorcontext.AsContext(err)...),
			zap.Error(err))
	}
}

Will output (stack lines omitted for brevity):

{"level":"warn","ts":1766772153.207416,"caller":"errorcontext/examples_test.go:63",
  "msg":"something failed",
  "error_context":{
    "panic":"panic: something bad happened",
    "stack":[".../runtime/panic.go:783 +0x120", "..."], "is_panic":true
  }, "error":"panic: something bad happened"}

Documentation

Index

Examples

Constants

View Source
const FieldNamePanicMessage = "panic"
View Source
const FieldNamePanicStackTrace = "stack"

Variables

This section is empty.

Functions

func Collect

func Collect[T error](err error) []T

Collect finds aggregates all errors that match the given target type, within the error chain of err. The resulting slice contains target error instances in reverse order.

func DefaultErrorGenerator

func DefaultErrorGenerator(p Panic) error

Types

type BaseError

type BaseError[T any] struct {
	// contains filtered or unexported fields
}

func NewBaseError

func NewBaseError[T any](originalErr error, initialContext T) *BaseError[T]

func (*BaseError[T]) ContextFields

func (e *BaseError[T]) ContextFields() T

func (*BaseError[T]) Error

func (e *BaseError[T]) Error() string

func (*BaseError[T]) IsPanic

func (e *BaseError[T]) IsPanic() bool

func (*BaseError[T]) IsZero

func (e *BaseError[T]) IsZero() bool

func (*BaseError[T]) MarkAsPanic

func (e *BaseError[T]) MarkAsPanic() *BaseError[T]

func (*BaseError[T]) SetContextFields

func (e *BaseError[T]) SetContextFields(f T)

SetContextFields is a setter that replaces the attached error context.

func (*BaseError[T]) Unwrap

func (e *BaseError[T]) Unwrap() error

Unwrap allows the original error to be resolved by errors.Is & errors.As.

type ErrorGenerator

type ErrorGenerator[T error] func(p Panic) T

type Panic

type Panic struct {
	Message string
	Stack   []string
}

type Recoverer

type Recoverer[T error] struct {
	PanicValueTransform func(r any) (string, error)
	NewErrorFunc        ErrorGenerator[T]
}
Example
package main

import (
	"fmt"

	"go.uber.org/zap"
	"golang.org/x/sync/errgroup"

	"github.com/georgepsarakis/errorcontext"
	zaperrorcontext "github.com/georgepsarakis/errorcontext/backend/zap"
)

func main() {
	cfg := zap.NewProductionConfig()
	zapLogger, err := cfg.Build()
	if err != nil {
		panic(err)
	}
	grp := errgroup.Group{}
	recoverer := errorcontext.Recoverer[*zaperrorcontext.Error]{
		NewErrorFunc: func(p errorcontext.Panic) *zaperrorcontext.Error {
			return zaperrorcontext.FromPanic(p)
		},
	}
	grp.Go(recoverer.WrapFunc(func() error {
		panic("something bad happened")
	}))

	grp.Go(func() error {
		fmt.Println("Hello World")
		return nil
	})
	if err := grp.Wait(); err != nil {
		zapLogger.Warn("something failed",
			zap.Dict("error_context", zaperrorcontext.AsContext(err)...),
			zap.Error(err))
	}
}
Output:

Hello World

func NewRecoverer

func NewRecoverer[T error](newError ErrorGenerator[T]) Recoverer[T]

func (Recoverer[T]) Format

func (r Recoverer[T]) Format(rv any) Panic

Format transforms an arbitrary value thrown by panic to an error message along with providing the current goroutine stack trace for the panic root cause. If PanicValueTransform is non-nil, an attempt to format the recovered value is performed. If the formatter function returns an error, a fallback approach is used and the failure error message is appended to the standard message template. Note: this method is intended to be public in order to facilitate testing.

func (Recoverer[T]) Wrap

func (r Recoverer[T]) Wrap(fn func() error) (err error)

Wrap allows recovery from panics for the given function. Panics are translated and propagated as errors that can be handled accordingly. Note: unrecovered panics can cause an abnormal program exit.

func (Recoverer[T]) WrapFunc

func (r Recoverer[T]) WrapFunc(fn func() error) func() error

WrapFunc is a convenience wrapper that returns a decorated function, ensuring that panics are converted to error values.

A common use case is to pass the function directly to errgroup.Submit:

grp := errgroup.Group{}
recoverer := errorcontext.Recoverer[error]{
	NewErrorFunc: errorcontext.DefaultErrorGenerator,
}
grp.Go(recoverer.WrapFunc(func() error {
	panic("something bad happened")
}))

Directories

Path Synopsis
backend
zap