goerr

package module
v1.0.0 Latest Latest
Warning

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

Go to latest
Published: Jan 7, 2025 License: BSD-2-Clause Imports: 9 Imported by: 140

README

goerr test gosec package scan Go Reference

Package goerr provides more contextual error handling in Go.

Features

goerr provides the following features:

  • Stack traces
    • Compatible with github.com/pkg/errors.
    • Structured stack traces with goerr.Stack is available.
  • Contextual variables to errors using With(key, value) and WithTags(tags ...Tag).
  • errors.Is to identify errors and errors.As to unwrap errors.
  • slog.LogValuer interface to output structured logs with slog.

Usage

Stack trace

goerr records stack trace when creating an error. The format is compatible with github.com/pkg/errors and it can be used for sentry.io, etc.

func someAction(fname string) error {
	if _, err := os.Open(fname); err != nil {
		return goerr.Wrap(err, "failed to open file")
	}
	return nil
}

func main() {
	if err := someAction("no_such_file.txt"); err != nil {
		log.Fatalf("%+v", err)
	}
}

Output:

2024/04/06 10:30:27 failed to open file: open no_such_file.txt: no such file or directory
main.someAction
        /Users/mizutani/.ghq/github.com/m-mizutani/goerr/examples/stacktrace_print/main.go:12
main.main
        /Users/mizutani/.ghq/github.com/m-mizutani/goerr/examples/stacktrace_print/main.go:18
runtime.main
        /usr/local/go/src/runtime/proc.go:271
runtime.goexit
        /usr/local/go/src/runtime/asm_arm64.s:1222
exit status 1

You can not only print the stack trace, but also extract the stack trace by goerr.Unwrap(err).Stacks().

if err := someAction("no_such_file.txt"); err != nil {
  // NOTE: `errors.Unwrap` also works
  if goErr := goerr.Unwrap(err); goErr != nil {
    for i, st := range goErr.Stacks() {
      log.Printf("%d: %v\n", i, st)
    }
  }
  log.Fatal(err)
}

Stacks() returns a slice of goerr.Stack struct, which contains Func, File, and Line.

2024/04/06 10:35:30 0: &{main.someAction /Users/mizutani/.ghq/github.com/m-mizutani/goerr/examples/stacktrace_extract/main.go 12}
2024/04/06 10:35:30 1: &{main.main /Users/mizutani/.ghq/github.com/m-mizutani/goerr/examples/stacktrace_extract/main.go 18}
2024/04/06 10:35:30 2: &{runtime.main /usr/local/go/src/runtime/proc.go 271}
2024/04/06 10:35:30 3: &{runtime.goexit /usr/local/go/src/runtime/asm_arm64.s 1222}
2024/04/06 10:35:30 failed to open file: open no_such_file.txt: no such file or directory
exit status 1

NOTE: If the error is wrapped by goerr multiply, %+v will print the stack trace of the deepest error.

Tips: If you want not to print the stack trace for current stack frame, you can use Unstack method. Also, UnstackN method removes the top multiple stack frames.

if err := someAction("no_such_file.txt"); err != nil {
	// Unstack() removes the current stack frame from the error message.
	return goerr.Wrap(err, "failed to someAction").Unstack()
}
Add/Extract contextual variables
Key-Value pairs

goerr provides the With(key, value) method to add contextual variables to errors. The standard way to handle errors in Go is by injecting values into error messages. However, this approach makes it difficult to aggregate various errors. On the other hand, goerr's With method allows for adding contextual information to errors without changing error message, making it easier to aggregate error logs. Additionally, error handling services like Sentry.io can handle errors more accurately with this feature.

var errFormatMismatch = errors.New("format mismatch")

func someAction(tasks []task) error {
	for _, t := range tasks {
		if err := validateData(t.Data); err != nil {
			return goerr.Wrap(err, "failed to validate data").With("name", t.Name)
		}
	}
	// ....
	return nil
}

func validateData(data string) error {
	if !strings.HasPrefix(data, "data:") {
		return goerr.Wrap(errFormatMismatch).With("data", data)
	}
	return nil
}

type task struct {
	Name string
	Data string
}

func main() {
	tasks := []task{
		{Name: "task1", Data: "data:1"},
		{Name: "task2", Data: "invalid"},
		{Name: "task3", Data: "data:3"},
	}
	if err := someAction(tasks); err != nil {
		if goErr := goerr.Unwrap(err); goErr != nil {
			for k, v := range goErr.Values() {
				log.Printf("var: %s => %v\n", k, v)
			}
		}
		log.Fatalf("msg: %s", err)
	}
}

Output:

2024/04/06 14:40:59 var: data => invalid
2024/04/06 14:40:59 var: name => task2
2024/04/06 14:40:59 msg: failed to validate data: : format mismatch
exit status 1

If you want to send the error to sentry.io with SDK, you can extract the contextual variables by goErr.Values() and set them to the scope.

// Sending error to Sentry
hub := sentry.CurrentHub().Clone()
hub.ConfigureScope(func(scope *sentry.Scope) {
  if goErr := goerr.Unwrap(err); goErr != nil {
    for k, v := range goErr.Values() {
      scope.SetExtra(k, v)
    }
  }
})
evID := hub.CaptureException(err)
Tags

There are use cases where we need to adjust the error handling strategy based on the nature of the error. A clear example is an HTTP server, where the status code to be returned varies depending on whether it's an error from a downstream system, a missing resource, or an unauthorized request. To handle this precisely, you could predefine errors for each type and use methods like errors.Is in the error handling section to verify and branch the processing accordingly. However, this approach becomes challenging as the program grows larger and the number and variety of errors increase.

goerr provides also WithTags(tags ...string) method to add tags to errors. Tags are useful when you want to categorize errors. For example, you can add tags like "critical" or "warning" to errors.

var (
	ErrTagSysError   = goerr.NewTag("system_error")
	ErrTagBadRequest = goerr.NewTag("bad_request")
)

func handleError(w http.ResponseWriter, err error) {
	if goErr := goerr.Unwrap(err); goErr != nil {
		switch {
		case goErr.HasTag(ErrTagSysError):
			w.WriteHeader(http.StatusInternalServerError)
		case goErr.HasTag(ErrTagBadRequest):
			w.WriteHeader(http.StatusBadRequest)
		default:
			w.WriteHeader(http.StatusInternalServerError)
		}
	} else {
		w.WriteHeader(http.StatusInternalServerError)
	}
	_, _ = w.Write([]byte(err.Error()))
}

func someAction() error {
	if _, err := http.Get("http://example.com/some/resource"); err != nil {
		return goerr.Wrap(err, "failed to get some resource").WithTags(ErrTagSysError)
	}
	return nil
}

func main() {
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		if err := someAction(); err != nil {
			handleError(w, err)
			return
		}
		w.WriteHeader(http.StatusOK)
		_, _ = w.Write([]byte("OK"))
	})

	http.ListenAndServe(":8090", nil)
}
Structured logging

goerr provides slog.LogValuer interface to output structured logs with slog. It can be used to output not only the error message but also the stack trace and contextual variables. Additionally, unwrapped errors can be output recursively.

var errRuntime = errors.New("runtime error")

func someAction(input string) error {
	if err := validate(input); err != nil {
		return goerr.Wrap(err, "failed validation")
	}
	return nil
}

func validate(input string) error {
	if input != "OK" {
		return goerr.Wrap(errRuntime, "invalid input").With("input", input)
	}
	return nil
}

func main() {
	logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
	if err := someAction("ng"); err != nil {
		logger.Error("aborted myapp", slog.Any("error", err))
	}
}

Output:

{
  "time": "2024-04-06T11:32:40.350873+09:00",
  "level": "ERROR",
  "msg": "aborted myapp",
  "error": {
    "message": "failed validation",
    "stacktrace": [
      "/Users/mizutani/.ghq/github.com/m-mizutani/goerr/examples/logging/main.go:16 main.someAction",
      "/Users/mizutani/.ghq/github.com/m-mizutani/goerr/examples/logging/main.go:30 main.main",
      "/usr/local/go/src/runtime/proc.go:271 runtime.main",
      "/usr/local/go/src/runtime/asm_arm64.s:1222 runtime.goexit"
    ],
    "cause": {
      "message": "invalid input",
      "values": {
        "input": "ng"
      },
      "stacktrace": [
        "/Users/mizutani/.ghq/github.com/m-mizutani/goerr/examples/logging/main.go:23 main.validate",
        "/Users/mizutani/.ghq/github.com/m-mizutani/goerr/examples/logging/main.go:15 main.someAction",
        "/Users/mizutani/.ghq/github.com/m-mizutani/goerr/examples/logging/main.go:30 main.main",
        "/usr/local/go/src/runtime/proc.go:271 runtime.main",
        "/usr/local/go/src/runtime/asm_arm64.s:1222 runtime.goexit"
      ],
      "cause": "runtime error"
    }
  }
}
Builder

goerr provides goerr.NewBuilder() to create an error with pre-defined contextual variables. It is useful when you want to create an error with the same contextual variables in multiple places.

type object struct {
	id    string
	color string
}

func (o *object) Validate() error {
	eb := goerr.NewBuilder().With("id", o.id)

	if o.color == "" {
		return eb.New("color is empty")
	}

	return nil
}

func main() {
	obj := &object{id: "object-1"}

	if err := obj.Validate(); err != nil {
		slog.Default().Error("Validation error", "err", err)
	}
}

Output:

2024/10/19 14:19:54 ERROR Validation error err.message="color is empty" err.values.id=object-1 (snip)

License

The 2-Clause BSD License. See LICENSE for more detail.

Documentation

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func Tags added in v0.3.0

func Tags(err error) []string

Tags returns list of tags that is set by WithTags. All wrapped goerr.Error tags will be merged. Tags of wrapped error is overwritten by upper goerr.Error.

func Values added in v0.3.0

func Values(err error) map[string]any

Values returns map of key and value that is set by With. All wrapped goerr.Error key and values will be merged. Key and values of wrapped error is overwritten by upper goerr.Error.

Types

type Builder added in v0.2.0

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

Builder keeps a set of key-value pairs and can create a new error and wrap error with the key-value pairs.

func NewBuilder added in v0.2.0

func NewBuilder() *Builder

NewBuilder creates a new Builder

func (*Builder) New added in v0.2.0

func (x *Builder) New(format string, args ...any) *Error

New creates a new error with message

func (*Builder) With added in v0.2.0

func (x *Builder) With(key string, value any) *Builder

With copies the current Builder and adds a new key-value pair.

func (*Builder) Wrap added in v0.2.0

func (x *Builder) Wrap(cause error, msg ...any) *Error

Wrap creates a new Error with caused error and add message.

type Error

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

Error is error interface for deepalert to handle related variables

func New

func New(format string, args ...any) *Error

New creates a new error with message

func Unwrap added in v0.1.6

func Unwrap(err error) *Error

Unwrap returns unwrapped goerr.Error from err by errors.As. If no goerr.Error, returns nil NOTE: Do not receive error interface. It causes typed-nil problem.

var err error = goerr.New("error")
if err != nil { // always true

func Wrap

func Wrap(cause error, msg ...any) *Error

Wrap creates a new Error and add message.

func Wrapf added in v0.1.12

func Wrapf(cause error, format string, args ...any) *Error

Wrapf creates a new Error and add message. The error message is formatted by fmt.Sprintf.

func (*Error) Error

func (x *Error) Error() string

Error returns error message for error interface

func (*Error) Format

func (x *Error) Format(s fmt.State, verb rune)

Format returns: - %v, %s, %q: formatted message - %+v: formatted message with stack trace

func (*Error) HasTag added in v0.3.0

func (x *Error) HasTag(tag Tag) bool

HasTag returns true if the error has the tag.

func (*Error) ID added in v0.1.6

func (x *Error) ID(id string) *Error

ID sets string to check equality in Error.IS()

func (*Error) Is added in v0.1.1

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

Is returns true if target is goerr.Error and Error.id of two errors are matched. It's for errors.Is. If Error.id is empty, it always returns false.

func (*Error) LogValue added in v0.1.9

func (x *Error) LogValue() slog.Value

LogValue returns slog.Value for structured logging. It's implementation of slog.LogValuer. https://pkg.go.dev/log/slog#LogValuer

func (*Error) Printable added in v0.1.3

func (x *Error) Printable() *Printable

Printable returns printable object

func (*Error) StackTrace

func (x *Error) StackTrace() StackTrace

StackTrace returns stack trace that is compatible with pkg/errors

func (*Error) Stacks

func (x *Error) Stacks() []*Stack

Stacks returns stack trace array generated by pkg/errors

func (*Error) Tags added in v0.3.0

func (x *Error) Tags() []string

Tags returns list of tags that is set by WithTags. All wrapped goerr.Error tags will be merged. Tags of wrapped error is overwritten by upper goerr.Error.

func (*Error) Unstack added in v0.1.14

func (x *Error) Unstack() *Error

Unstack trims stack trace by 1. It can be used for internal helper or utility functions.

func (*Error) UnstackN added in v0.1.14

func (x *Error) UnstackN(n int) *Error

UnstackN trims stack trace by n. It can be used for internal helper or utility functions.

func (*Error) Unwrap

func (x *Error) Unwrap() error

Unwrap returns *fundamental of github.com/pkg/errors

func (*Error) Values

func (x *Error) Values() map[string]any

Values returns map of key and value that is set by With. All wrapped goerr.Error key and values will be merged. Key and values of wrapped error is overwritten by upper goerr.Error.

func (*Error) With

func (x *Error) With(key string, value any) *Error

With adds key and value related to the error event

func (*Error) WithTags added in v0.3.0

func (x *Error) WithTags(tags ...Tag) *Error

WithTags adds tags to the error. The tags are used to categorize errors.

func (*Error) Wrap added in v0.1.1

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

Wrap creates a new Error and copy message and id to new one.

type Printable added in v0.3.0

type Printable struct {
	Message    string         `json:"message"`
	ID         string         `json:"id"`
	StackTrace []*Stack       `json:"stacktrace"`
	Cause      any            `json:"cause"`
	Values     map[string]any `json:"values"`
	Tags       []string       `json:"tags"`
}

type Stack

type Stack struct {
	Func string `json:"func"`
	File string `json:"file"`
	Line int    `json:"line"`
}

Stack represents function, file and line No of stack trace

type StackTrace

type StackTrace []frame

StackTrace is array of frame. It's exported for compatibility with github.com/pkg/errors

func (StackTrace) Format

func (st StackTrace) Format(s fmt.State, verb rune)

Format formats the stack of Frames according to the fmt.Formatter interface.

%s	lists source files for each Frame in the stack
%v	lists the source file and line number for each Frame in the stack

Format accepts flags that alter the printing of some verbs, as follows:

%+v   Prints filename, function, and line number for each Frame in the stack.

type Tag added in v0.3.0

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

Tag is a type to represent an error tag. It is used to categorize errors. The struct should be created by only NewTag function.

Example:

TagNotFound := NewTag("not_found")

func FindUser(id string) (*User, error) {
	...
	if user == nil {
		return nil, goerr.New("user not found").WithTags(TagNotFound)
	}
	...
}

func main() {
	err := FindUser("123")
	if goErr := goerr.Unwrap(err); goErr != nil {
		if goErr.HasTag(TagNotFound) {
			fmt.Println("User not found")
		}
	}
}

func NewTag added in v0.3.0

func NewTag(value string) Tag

NewTag creates a new Tag. The key will be empty.

Example
package main

import (
	"fmt"

	"github.com/m-mizutani/goerr"
)

func main() {
	t1 := goerr.NewTag("DB error")
	err := goerr.New("error message").WithTags(t1)

	if goErr := goerr.Unwrap(err); goErr != nil {
		if goErr.HasTag(t1) {
			fmt.Println("DB error")
		}
	}
}
Output:

DB error

func (Tag) Format added in v0.3.0

func (t Tag) Format(s fmt.State, verb rune)

Format writes the Tag to the writer. It's for implementing fmt.Formatter interface.

func (Tag) String added in v0.3.0

func (t Tag) String() string

String returns the string representation of the Tag. It's for implementing fmt.Stringer interface.

Directories

Path Synopsis
examples
tag
stacktrace Module

Jump to

Keyboard shortcuts

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