serrors

package module
v0.1.1 Latest Latest
Warning

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

Go to latest
Published: Jan 25, 2024 License: MIT Imports: 7 Imported by: 0

README

serrors - Structured Errors

Actions Status Coverage Status PkgGoDev go-report

serrors allows you to add tags/fields/kv-pairs to your errors.

Usage

package main

import (
	"os"
	"fmt"
	"log/slog"
	
	"github.com/Eun/serrors"
)

func validateUserName(name string) error {
	const maxLength = 10
	if len(name) > maxLength {
		return serrors.New("username is too long").
			With("username", name).
			With("max_length", maxLength)
	}
	return nil
}

func main() {
	user := os.Getenv("USER")
	err := validateUserName(user)
	if err != nil {
		slog.Error("name validation failed",
			"error", err.Error(),
			slog.Group("details", serrors.GetFieldsAsCombinedSlice(err)...),
			"stack", serrors.GetStack(err),
		)
		return
	}
	fmt.Println("Welcome ", user)
}

Problem

We use structured loggers like slog to create nice formatted log messages
and add important context to error messages.
Take this code as an example:

func validateUserNameLength(name string) error {
	const maxLength = 10
	if len(name) > maxLength {
		slog.Error("username is too long", "username", name, "max_length", maxLength)
		return errors.New("username is too long")
	}
	return nil
}

Not only do we return an error, but we also log the error using slog.
Lets look at the calling function:

func addUserToRole(userName, roleName string) error {
	if err := validateUserNameLength(userName); err != nil {
		slog.Error("validation of username failed", "username", name)
		return fmt.Errorf("validation of username failed: %w", err)
	}
	// ...
}

Again, we return the error (with the underlying error), and we also log it - because we need the context in our messages.

In this case we end up with at least two error messages:

  1. slog.Error("username is too long", ...)
  2. slog.Error("validation of username failed", ...)
  3. when we handle addUserToRole: validation of username failed: username is too long

The last error that will be logged or printed won't contain any useful information on why this problem actually occurred.

One possible solution would be to use something like fmt.Errorf("username is too long [username=%s]", name). However, this could lead to some funny unreadable errors like:

validation of username failed [username=MisterDolittle]: username is too long [username=MisterDolittle] [max_length=10]

This package attempts to solve this problem by providing methods to add tags/fields/kv-pairs to errors that can later be retrieved.

Builder Usage

You could save some code duplication by using the builder functionality:

func validateUserName(name string) error {
	serr := serrors.NewBuilder().
		With("username", name)

	if name == "" {
		return serr.New("username cannot be empty")
	}

	if err := validateUserNameLength(name); err != nil {
		return serr.Wrap(err, "username has invalid length")
	}

	reservedNames := []string{"root", "admin"}
	for _, s := range reservedNames {
		if name == s {
			return serr.Errorf("username cannot be %q, it is reserved", name).
				With("reserved", reservedNames)
		}
	}
	return nil
}

Building without Stack

By default serrors collects stack information, this behaviour can be disabled by setting the build tag serrors_without_stack:

go build -tags serrors_without_stack ...

Documentation

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func GetFields

func GetFields(err error) map[string]any

GetFields will return all fields that are added to the specified error.

func GetFieldsAsCombinedSlice

func GetFieldsAsCombinedSlice(err error) []any

GetFieldsAsCombinedSlice will return all fields as a slice that are added to the specified error. The format will be [key1, value1, key2, value2, ..., keyN, valueN].

Types

type Error

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

Error is the error that will be returned by ErrorBuilder and the functions New, Errorf, Wrap and Wrapf. It implements the stdlib error interface.

Example
package main

import (
	"log/slog"
	"slices"

	"github.com/Eun/serrors"
)

func validateUserNameLength(name string) error {
	const maxLength = 10
	if len(name) > maxLength {
		return serrors.New("username is too long").
			With("username", name).
			With("max_length", maxLength)
	}
	return nil
}

func validateUserName(name string) error {
	serr := serrors.NewBuilder().
		With("username", name)

	if name == "" {
		return serr.New("username cannot be empty")
	}

	if err := validateUserNameLength(name); err != nil {
		return serr.Wrap(err, "username has invalid length")
	}

	reservedNames := []string{"root", "admin"}
	for _, s := range reservedNames {
		if name == s {
			return serr.Errorf("username cannot be %q, it is reserved", name).
				With("reserved", reservedNames)
		}
	}
	return nil
}

func addUserToRole(userName, roleName string) error {
	if err := validateUserName(userName); err != nil {
		return serrors.Wrap(err, "validation of username failed").With("username", userName)
	}

	if roleName == "" {
		return serrors.New("rolename cannot be empty")
	}
	availableRoles := []string{"admin", "user"}
	if !slices.Contains(availableRoles, roleName) {
		return serrors.Errorf("unknown role %q", roleName).
			With("username", userName).
			With("available_roles", availableRoles)
	}

	// todo: add user to role
	// ...

	return nil
}

func main() {
	if err := addUserToRole("joe", "guest"); err != nil {
		slog.Error("name validation failed",
			"error", err.Error(),
			slog.Group("details", serrors.GetFieldsAsCombinedSlice(err)...),
			"stack", serrors.GetStack(err),
		)
		return
	}
}
Output:

func Errorf

func Errorf(format string, a ...any) *Error

Errorf creates a new Error with the supplied message formatted according to a format specifier.

Example
package main

import (
	"fmt"

	"github.com/Eun/serrors"
)

func main() {
	name := "Joe"
	if name == "alice" {
		panic(serrors.Errorf("invalid name %q", name).With("name", name))
	}
	fmt.Println(name)
}
Output:

Joe

func New

func New(message string) *Error

New creates a new Error with the supplied message.

Example
package main

import (
	"fmt"

	"github.com/Eun/serrors"
)

func main() {
	name := "Joe"
	if name == "alice" {
		panic(serrors.New("invalid name").With("name", name))
	}
	fmt.Println(name)
}
Output:

Joe

func Wrap

func Wrap(err error, message string) *Error

Wrap creates a new Error with the supplied message. The passed in error will be added as a cause for this error.

Example
name := "Joe"
if err := validateUserName(name); err != nil {
	panic(serrors.Wrap(err, "invalid name").With("name", name))
}
fmt.Println(name)
Output:

Joe

func Wrapf

func Wrapf(err error, format string, a ...any) *Error

Wrapf creates a new Error with the supplied message formatted according to a format specifier. The passed in error will be added as a cause for this error.

Example
name := "Joe"
if err := validateUserName(name); err != nil {
	panic(serrors.Wrapf(err, "invalid name %q", name).With("name", name))
}
fmt.Println(name)
Output:

Joe

func (*Error) Cause

func (e *Error) Cause() error

Cause returns the cause of this error.

func (*Error) Error

func (e *Error) Error() string

Error returns the error string representation including the cause of this error.

func (*Error) Format

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

Format formats the error according to the format specifier.

func (*Error) Unwrap

func (e *Error) Unwrap() error

Unwrap provides compatibility for Go 1.13 error chains.

func (*Error) With

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

With adds the field key with the value to the error fields.

type ErrorBuilder

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

ErrorBuilder is a type that provides a way to build errors.

func NewBuilder

func NewBuilder() *ErrorBuilder

NewBuilder creates a new ErrorBuilder. To build the error use either ErrorBuilder.New, ErrorBuilder.Errorf, ErrorBuilder.Wrap or ErrorBuilder.Wrapf.

Example
package main

import (
	"fmt"

	"github.com/Eun/serrors"
)

func main() {
	name := "Joe"
	serr := serrors.NewBuilder().
		With("username", name)

	if name == "alice" {
		panic(serr.New("username cannot be alice"))
	}
	fmt.Println(name)
}
Output:

Joe

func (*ErrorBuilder) Errorf

func (eb *ErrorBuilder) Errorf(format string, a ...any) *Error

Errorf creates a new Error with the supplied message formatted according to a format specifier. The error will contain all fields that were previously passed to ErrorBuilder.

Example
package main

import (
	"fmt"

	"github.com/Eun/serrors"
)

func main() {
	name := "Joe"
	serr := serrors.NewBuilder().
		With("username", name)
	if name == "alice" {
		panic(serr.Errorf("username cannot be %q", name))
	}
	fmt.Println(name)
}
Output:

Joe

func (*ErrorBuilder) New

func (eb *ErrorBuilder) New(message string) *Error

New creates a new Error with the supplied message. The error will contain all fields that were previously passed to ErrorBuilder.

Example
package main

import (
	"fmt"

	"github.com/Eun/serrors"
)

func main() {
	name := "Joe"
	serr := serrors.NewBuilder().
		With("username", name)
	if name == "alice" {
		panic(serr.New("username cannot be alice"))
	}
	fmt.Println(name)
}
Output:

Joe

func (*ErrorBuilder) With

func (eb *ErrorBuilder) With(key string, value any) *ErrorBuilder

With adds the field key with the value to the error fields.

Example
package main

import (
	"fmt"

	"github.com/Eun/serrors"
)

func main() {
	name := "Joe"
	serr := serrors.NewBuilder()
	serr = serr.With("username", name)
	if name == "alice" {
		panic(serr.New("username cannot be alice"))
	}
	fmt.Println(name)
}
Output:

Joe

func (*ErrorBuilder) Wrap

func (eb *ErrorBuilder) Wrap(err error, message string) *Error

Wrap creates a new Error with the supplied message. The passed in error will be added as a cause for this error. The error will contain all fields that were previously passed to ErrorBuilder.

Example
name := "Joe"
serr := serrors.NewBuilder().
	With("username", name)
if err := validateUserName(name); err != nil {
	panic(serr.Wrap(err, "validation of username failed"))
}
fmt.Println(name)
Output:

Joe

func (*ErrorBuilder) Wrapf

func (eb *ErrorBuilder) Wrapf(err error, format string, a ...any) *Error

Wrapf creates a new Error with the supplied message formatted according to a format specifier. The passed in error will be added as a cause for this error. The error will contain all fields that were previously passed to ErrorBuilder.

Example
name := "Joe"
serr := serrors.NewBuilder().
	With("username", name)
if err := validateUserName(name); err != nil {
	panic(serr.Wrapf(err, "validation of username %q failed", name))
}
fmt.Println(name)
Output:

Joe

type ErrorStack

type ErrorStack struct {
	ErrorMessage string         `json:"error_message" yaml:"error_message"`
	Fields       map[string]any `json:"fields" yaml:"fields"`
	StackTrace   []StackFrame   `json:"stack_trace" yaml:"stack_trace"`
	// contains filtered or unexported fields
}

ErrorStack holds an error and its relevant information.

func GetStack

func GetStack(err error) []ErrorStack

GetStack returns the errors that are present in the provided error.

func (*ErrorStack) Error

func (es *ErrorStack) Error() error

Error returns the main error.

type StackFrame

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

StackFrame represents a single stack frame of an error.

Jump to

Keyboard shortcuts

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