erk

package module
v0.5.11 Latest Latest
Warning

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

Go to latest
Published: Oct 25, 2022 License: MIT Imports: 8 Imported by: 4

README

erk

Errors with kinds for Go 1.13+.

Documentation CI Go Report Card codecov

Install

$ go get github.com/JosiahWitt/erk

About

Erk allows you to create errors that have a kind, message template, and params.

Since Erk supports Go 1.13+ errors.Is, it is easier to test errors, especially errors that contain parameters.

Erk is quite extensible by leveraging the fact that kinds are struct types. For example, HTTP status codes, distinguishing between warnings and errors, and more can easily be embedded in kinds. See advanced kinds for some examples.

The name "erk" comes from "errors with kinds". Erk is also a play on irk, since errors can be annoying to deal with. Hopefully Erk makes them less irksome. 😄

Overview

Error Kinds

Error kinds are struct types that implement the Kind interface. Typically the Kind interface is satisfied by embedding a default kind, such as erk.DefaultKind. It is recommended to define a default kind for your app or package.

Example: type ErkTableMissing struct { erk.DefaultKind }

Message Templates

Error messages are text templates, which allows referencing params by name. Since params are stored in map, this is done by using the {{.paramName}} notation.

Example: "table {{.tableName}} does not exist"

Template Functions

A few functions in addition to the built in template functions have been added.

  • type: Returns the type of the param. It is equivalent to fmt.Sprintf("%T", param)

    Example: {{type .paramName}}

  • inspect: Returns more details for complex types. It is equivalent to fmt.Sprintf("%+v", param)

    Example: {{inspect .paramName}}

Extending Template Functions

Template functions can be extended by overriding the TemplateFuncsFor method on your default kind.

Params

Params allow adding arbitrary context to errors. Params are stored as a map, and can be referenced in templates.

Wrapping Errors

Other errors can be wrapped into Erk errors using the erk.Wrap, erk.WrapAs, and erk.WrapWith, functions. (I recommend defining errors as public variables, and avoid using erk.Wrap.)

The wrapped error is stored in the params by the err key. Thus, templates can reference the error they wrap by using {{.err}}.

Use errors.Unwrap to return the original error.

Error Groups

Errors can be grouped using the erg package.

Errors are appended to the error group as they are encountered. Be sure to conditionally return the error group by calling erg.Any, otherwise a non-nil error group with no errors will be returned.

See the example below.

Testing

Since Erk supports Go 1.13+ errors.Is, testing errors is straightforward. This is especially helpful for comparing errors that leverage parameters, since the parameters are ignored. (Usually you just want to test a certain error was returned from the function, not that the error is assembled correctly.)

Example: errors.Is(err, mypkg.ErrTableDoesNotExist) returns true only if the err is mypkg.ErrTableDoesNotExist

Mocking

When returning an Erk error from a mock, most of the time the required template parameters are not critical to the test. However, if the code being tested uses errors.Is, and strict mode is enabled, simply returning the error from the mock will result in a panic.

Example: someMockedFunction.Returns(store.ErrItemNotFound) might panic

Thus, the erkmock package exists to support returning errors from mocks without setting the required parameters. You can create a mocked error From an existing Erk error, or For an error kind.

Example: someMockedFunction.Returns(erkmock.From(store.ErrItemNotFound)) does not panic

Strict Mode

By default, strict mode is not enabled. Thus, if errors are encountered while rendering the error (eg. invalid template), the unrendered template is silently returned. If parameters are missing for the template, <no value> is used instead. This makes sense in production, as an unrendered template is better than returning a render error.

However, when testing or in development mode, it might be useful for these types of issues to be more visible.

Strict mode causes a panic when it encounters an invalid template or missing parameters. It is automatically enabled in tests, and can be explicitly enabled or disabled using the ERK_STRICT_MODE environment variable set to true or false, respectively. It can also be enabled or disabled programmatically by using the erkstrict.SetStrictMode function.

When strict mode is enabled, calls to errors.Is will also attempt to render the error. This is useful in tests.

JSON Errors

Errors created with Erk can be directly marshaled to JSON, since the MarshalJSON method is present.

Internally, this calls erk.Export, followed by json.Marshal.

If you want to customize how errors are marshalled to JSON, simply write your own function that uses erk.Export and modifies the exported error as necessary before marshalling JSON.

If not all errors in your application are guaranteed to be erk errors, calling erk.Export before marshalling to JSON will ensure each error is explicitly converted to an erk error.

If you would like to export the errors as JSON, and return the error kind as the error type, see erkjson. Using the error kind as the exported error type is useful for something like AWS Step Functions, which allows defining retry policies based on the type of the returned error.

Advanced Kinds

Since error kinds are struct types, they can embed other structs. This allows quite a bit of flexibility.

Warnings

For example, you could create an erkwarning package that defines a struct with an IsWarning() bool method. Then, you can use an interface to check for that method, and if the method returns true, log the error instead of returning it to the client. This would work well when coupled with erg. Any error kind that should be a warning simply needs to embed the struct from erkwarning. This allows all errors to bubble to the top, simplifying how warnings and errors are distinguished.

HTTP Statuses

Something similar can also be done for HTTP statuses, allowing status codes to be determined on the error kind level.

See erkhttp for an implementation.

Recommendations

Default Error Kind

It is recommended to define a default error kind for your app or package that embeds erk.DefaultKind. Then, every error kind for your app or package can embed that default error kind. This allows easily overriding or adding properties to the default kind.

Two recommended names for this shared package are erks or errkinds.

Example: type Default struct { erk.DefaultKind }

Defining Error Kinds

There are two recommended ways to define your kinds:

  1. Define your error kind types in each package near the errors themselves.

    This allows erk.Export or erk.GetKindString to contain which package the error kind was defined, and therefore, where the error originated.

  2. Define a package that contains all error kinds, and override the default error kind's KindStringFor method to return a snake case version of each kind's type.

    This produces a nicer API for consumers, and allows you to move around error kinds without changing the string emitted by the API.

    If using this method in a package, it may be a good idea to prefix with your package name to prevent collisions.

Defining Errors

It is recommended to define every error as a public variable, so consumers of your package can check against each error. Avoid defining errors inside of functions.

Examples

Error Kinds

You can create errors with kinds using the erk package.

package store

import "github.com/JosiahWitt/erk"

type (
  ErkMissingKey struct { erk.DefaultKind }
  ...
)

var (
  ErrMissingReadKey = erk.New(ErkMissingKey{}, "no read key specified for table '{{.tableName}}'")
  ErrMissingWriteKey = erk.New(ErkMissingKey{}, "no write key specified for table '{{.tableName}}'")
  ...
)

func Read(tableName, key string, data interface{}) error {
  ...

  if key == "" {
    return erk.WithParam(ErrMissingReadKey, "tableName", tableName)
  }

  ...
}
package main

...

func main() {
  err := store.Read("my_table", "", nil)

  bytes, _ := json.MarshalIndent(erk.Export(err), "", "  ")
  fmt.Println(string(bytes))

  fmt.Println()
  fmt.Println("erk.IsKind(err, store.ErkMissingKey{}):  ", erk.IsKind(err, store.ErkMissingKey{}))
  fmt.Println("errors.Is(err, store.ErrMissingReadKey): ", errors.Is(err, store.ErrMissingReadKey))
  fmt.Println("errors.Is(err, store.ErrMissingWriteKey):", errors.Is(err, store.ErrMissingWriteKey))
}
Output
{
  "kind": "github.com/username/repo/store:ErkMissingKey",
  "message": "no read key specified for table 'my_table'",
  "params": {
    "tableName": "my_table"
  }
}

erk.IsKind(err, store.ErkMissingKey{}):   true
errors.Is(err, store.ErrMissingReadKey):  true
errors.Is(err, store.ErrMissingWriteKey): false
Error Groups

You can also wrap a group of errors using the erg package.

package store

import "github.com/JosiahWitt/erk"

type (
  ErkMultiRead struct { erk.DefaultKind }
  ...
)

var (
  ErrUnableToMultiRead = erk.New(ErkMultiRead{}, "could not multi read from '{{.tableName}}'")
  ...
)

func MultiRead(tableName string, keys []string, data interface{}) error {
  ...

  groupErr := erg.NewAs(ErrUnableToMultiRead)
  groupErr = erk.WithParam(groupErr, "tableName", tableName)
  for _, key := range keys {
    groupErr = erg.Append(groupErr, Read(tableName, key, data))
  }
  if erg.Any(groupErr) {
    return groupErr
  }

  ...
}
package main

...

func main() {
  err := store.MultiRead("my_table", []string{"", "my key", ""}, nil)

  bytes, _ := json.MarshalIndent(erk.Export(err), "", "  ")
  fmt.Println(string(bytes))

  fmt.Println()
  fmt.Println("erk.IsKind(err, store.ErkMultiRead{}):     ", erk.IsKind(err, store.ErkMultiRead{}))
  fmt.Println("errors.Is(err, store.ErrUnableToMultiRead):", errors.Is(err, store.ErrUnableToMultiRead))
  fmt.Println("errors.Is(err, store.ErrMissingReadKey):   ", errors.Is(err, store.ErrMissingReadKey))
  fmt.Println("errors.Is(err, store.ErrMissingWriteKey):  ", errors.Is(err, store.ErrMissingWriteKey))
}
Output
{
  "kind": "github.com/username/repo/store:ErkMultiRead",
  "message": "could not multi read from 'my_table':\n - no read key specified for table 'my_table'\n - no read key specified for table 'my_table'",
  "params": {
    "tableName": "my_table"
  },
  "header": "could not multi read from 'my_table'",
  "errors": [
    "no read key specified for table 'my_table'",
    "no read key specified for table 'my_table'"
  ]
}

erk.IsKind(err, store.ErkMultiRead{}):      true
errors.Is(err, store.ErrUnableToMultiRead): true
errors.Is(err, store.ErrMissingReadKey):    false
errors.Is(err, store.ErrMissingWriteKey):   false

Documentation

Overview

Package erk defines errors with kinds for Go 1.13+.

Index

Constants

View Source
const IndentSpaces = "  "

IndentSpaces are the spaces to indent errors.

View Source
const OriginalErrorParam = "err"

OriginalErrorParam is the param key that contains the wrapped error.

This allows the original error to be used in message templates. Also, errors can be unwrapped, using errors.Unwrap(err).

Variables

This section is empty.

Functions

func GetKindString added in v0.2.0

func GetKindString(err error) string

GetKindString returns a string identifying the kind of the error.

If the kind embeds erk.DefaultKind, this will be a string with the package and type of the error's kind. This string can be overridden by implementing a KindStringFor method on a base kind, and embedding that in the error kind.

erk.DefaultKind Example:

erk.GetKindString(err) // Output: "github.com/username/package:ErkYourKind"

func IsKind added in v0.2.0

func IsKind(err error, kind Kind) bool

IsKind checks if the error's kind is the provided kind.

func New added in v0.2.0

func New(kind Kind, message string) error

New creates an error with a kind and message.

func NewWith added in v0.2.0

func NewWith(kind Kind, message string, params Params) error

NewWith creates an error with a kind, message, and params.

func WithParam added in v0.2.0

func WithParam(err error, key string, value interface{}) error

WithParam adds a parameter to an error.

If err does not satisfy Paramable, the original error is returned. A nil param value deletes the param key.

func WithParams added in v0.2.0

func WithParams(err error, params Params) error

WithParams adds parameters to an error.

If err does not satisfy Paramable, the original error is returned. A nil param value deletes the param key.

func Wrap added in v0.2.0

func Wrap(kind Kind, message string, err error) error

Wrap an error with a kind and message.

func WrapAs added in v0.2.0

func WrapAs(erkError error, err error) error

WrapAs wraps an error as an erk error.

func WrapWith added in v0.5.0

func WrapWith(erkError error, err error, params Params) error

WrapWith wraps an error as an erk error with params.

It is equalent to calling erk.WithParams(erk.WrapAs(erkError, err), erk.Params{}).

Types

type BaseExport added in v0.3.0

type BaseExport struct {
	Kind    string `json:"kind"`
	Message string `json:"message"`
	Params  Params `json:"params,omitempty"`
}

BaseExport error that satisfies the ExportedErkable interface and is useful for JSON marshalling.

func (*BaseExport) ErrorKind added in v0.3.0

func (e *BaseExport) ErrorKind() string

ErrorKind returns the error kind.

func (*BaseExport) ErrorMessage added in v0.3.0

func (e *BaseExport) ErrorMessage() string

ErrorMessage returns the error message.

func (*BaseExport) ErrorParams added in v0.3.0

func (e *BaseExport) ErrorParams() Params

ErrorParams returns the error params.

type DefaultKind added in v0.2.0

type DefaultKind struct{}

DefaultKind should be embedded in most Kinds.

It is recommended to create new error kinds in each package. This allows erk to get the package name the error occurred in.

Example: See Kind.

func (DefaultKind) CloneKind added in v0.5.1

func (DefaultKind) CloneKind(kind Kind) Kind

CloneKind to a shallow copy.

If the kind is not a pointer, it is directly returned (since it was passed by value). If the kind is not a struct, it is directly returned (not supported for now). Otherwise, a shallow new copy of the struct is created using reflection, and the first layer of the struct is copyied using Set.

func (DefaultKind) KindStringFor added in v0.3.5

func (DefaultKind) KindStringFor(kind Kind) string

KindStringFor the provided kind.

func (DefaultKind) TemplateFuncsFor added in v0.5.0

func (DefaultKind) TemplateFuncsFor(kind Kind) template.FuncMap

TemplateFuncsFor the provided kind.

type DefaultPtrKind added in v0.5.2

type DefaultPtrKind struct{}

DefaultPtrKind is equivalent to DefaultKind, but enforces that the kinds are pointers.

func (*DefaultPtrKind) CloneKind added in v0.5.2

func (*DefaultPtrKind) CloneKind(kind Kind) Kind

CloneKind to a shallow copy.

func (*DefaultPtrKind) KindStringFor added in v0.5.2

func (*DefaultPtrKind) KindStringFor(kind Kind) string

KindStringFor the provided kind.

func (*DefaultPtrKind) TemplateFuncsFor added in v0.5.2

func (*DefaultPtrKind) TemplateFuncsFor(kind Kind) template.FuncMap

TemplateFuncsFor the provided kind.

type Erkable added in v0.3.0

type Erkable interface {
	Paramable
	Kindable
	Exportable
	error
}

Erkable errors that have Params and a Kind, and can be exported.

func ToErk added in v0.3.0

func ToErk(err error) Erkable

ToErk converts an error to an erk.Erkable by wrapping it in an erk.Error. If it is already an erk.Erkable, it returns the error without wrapping it.

type Error added in v0.2.0

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

Error stores details about an error with kinds and a message template.

func (*Error) Error added in v0.2.0

func (e *Error) Error() string

Error processes the message template with the provided params.

func (*Error) Export added in v0.3.0

func (e *Error) Export() ExportedErkable

Export creates a visible copy of the Error that can be used outside the erk package. A common use case is marshalling the error to JSON.

func (*Error) ExportRawMessage added in v0.5.6

func (e *Error) ExportRawMessage() string

ExportRawMessage without executing the template.

func (*Error) IndentError added in v0.4.1

func (e *Error) IndentError(indentLevel string) string

IndentError processes the message template with the provided params and indentation.

The indentLevel represents the indentation of wrapped errors. Thus, it should start with " ".

func (*Error) Is added in v0.2.0

func (e *Error) Is(err error) bool

Is implements the Go 1.13+ Is interface for use with errors.Is.

func (*Error) Kind added in v0.3.0

func (e *Error) Kind() Kind

Kind returns a copy of the Error's Kind.

func (*Error) MarshalJSON added in v0.4.2

func (e *Error) MarshalJSON() ([]byte, error)

MarshalJSON by exporting the error and then marshalling.

func (*Error) Params added in v0.3.0

func (e *Error) Params() Params

Params returns a copy of the Error's Params.

func (*Error) Unwrap added in v0.2.0

func (e *Error) Unwrap() error

Unwrap implements the Go 1.13+ Unwrap interface for use with errors.Unwrap.

func (*Error) WithParams added in v0.3.0

func (e *Error) WithParams(params Params) error

WithParams adds parameters to a copy of the Error.

A nil param value deletes the param key.

type ErrorIndentable added in v0.4.1

type ErrorIndentable interface {
	IndentError(indentLevel string) string
}

ErrorIndentable allows you to specify an indent level for an error.

type Exportable added in v0.3.0

type Exportable interface {
	ExportRawMessage() string
	Export() ExportedErkable
}

Exportable errors that support being exported to a JSON marshal friendly format.

type ExportedErkable added in v0.3.0

type ExportedErkable interface {
	ErrorMessage() string
	ErrorKind() string
	ErrorParams() Params
}

ExportedErkable is an exported readonly version of the Erkable interface.

func Export added in v0.3.0

func Export(err error) ExportedErkable

Export creates a visible copy of the error that can be used outside the erk package. A common use case is marshalling the error to JSON. If err is not an erk.Erkable, it is wrapped first.

type ExportedError added in v0.3.0

type ExportedError struct {
	Kind    *string `json:"kind"`
	Type    *string `json:"type,omitempty"`
	Message string  `json:"message"`
	Params  Params  `json:"params,omitempty"`

	ErrorStack []ExportedErkable `json:"errorStack,omitempty"`
}

ExportedError that can be used outside the erk package. A common use case is marshalling the error to JSON.

func (*ExportedError) ErrorKind added in v0.5.8

func (e *ExportedError) ErrorKind() string

ErrorKind returns the error kind.

func (*ExportedError) ErrorMessage added in v0.5.8

func (e *ExportedError) ErrorMessage() string

ErrorMessage returns the error message.

func (*ExportedError) ErrorParams added in v0.5.8

func (e *ExportedError) ErrorParams() Params

ErrorParams returns the error params.

type Kind added in v0.2.0

type Kind interface {
	KindStringFor(Kind) string
}

Kind represents an error kind.

Example:

package hello

type (
  ErkJSONUnmarshalling struct { erk.DefaultKind }
  ErkJSONMarshalling   struct { erk.DefaultKind }
)

...

// Creating an error with the kind
err := erk.New(ErkJSONUnmarshalling, "failed to unmarshal JSON: '{{.json}}'") // Usually this would be a global error variable
err = erk.WithParams(err, "json", originalJSON)

...

func GetKind added in v0.2.0

func GetKind(err error) Kind

GetKind from the provided error.

type Kindable added in v0.3.0

type Kindable interface {
	Kind() Kind
}

Kindable errors that support housing an error Kind.

type Paramable added in v0.3.0

type Paramable interface {
	WithParams(params Params) error
	Params() Params
}

Paramable errors that support appending Params and getting Params.

type Params added in v0.2.0

type Params map[string]interface{}

Params are key value parameters that are usuable in the message template.

func GetParams added in v0.2.0

func GetParams(err error) Params

GetParams returns the error's parameters.

If err does not satisfy Paramable, nil is returned.

func (Params) Clone added in v0.3.2

func (p Params) Clone() Params

Clone the params into a copy.

func (Params) MarshalJSON added in v0.3.2

func (p Params) MarshalJSON() ([]byte, error)

MarshalJSON by converting the "err" element to a string.

Directories

Path Synopsis
Package erg allows grouping errors into an error group.
Package erg allows grouping errors into an error group.
Package erkjson allows exporting errors as JSON.
Package erkjson allows exporting errors as JSON.
Package erkmock allows creating erk errors to be returned from mocked interfaces.
Package erkmock allows creating erk errors to be returned from mocked interfaces.
Package erkstrict controls if erk is running in strict mode.
Package erkstrict controls if erk is running in strict mode.

Jump to

Keyboard shortcuts

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