Git Product home page Git Product logo

erk's Introduction

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

erk's People

Contributors

josiahwitt avatar nc-wittj avatar

Stargazers

 avatar  avatar  avatar

Watchers

 avatar  avatar

erk's Issues

Export an errorStack

When wrapping an erk error, the original err field is overwritten with the first Erk error. Promote the original err to rootErr, so the original error is not lost.

Create an interface for Kinds

Currently erk.Kind is an empty interface, meaning we get no type checking. Let's add a method to that interface, which would be implemented on erk.DefaultKind. That switch would require creating kinds like (and would be a breaking change):

type ErkMyKind struct { erk.DefaultKind }

Using struct embedding is useful for other things, like HTTP statuses, or any other metadata you want to attach to the kind (like marking certain kinds as warnings).

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.