simplerr

package module
v0.1.10 Latest Latest
Warning

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

Go to latest
Published: Jun 23, 2022 License: MIT Imports: 3 Imported by: 0

README

Go Reference Github tag Go version Build Status Go Report Card gopherbadger-tag-do-not-edit Lint Status

Simplerr

Simplerr provides a simple and more powerful Go error handling experience by providing an alternative error implementation, the SimpleError. Simplerr was designed to be convenient and highly configurable. The main goals of Simplerr is to reduce boilerplate and make error handling and debugging easier.

Check out the blog post introducing why simplerr was created.

Features

The SimpleError allows you to easily:

  • Apply an error code to any error. Choose from a list of standard codes or register your own.
  • Automatically translate simplerr (including custom codes) error codes to other standardized codes such as HTTP/gRPC via middleware.
  • Attach key-value pairs to errors that can be used with structured loggers.
  • Attach and check for custom attributes similar to the context package.
  • Automatically capture stack traces at the point the error is raised.
  • Mark errors as silent so they can be skipped by logging middleware.
  • Mark errors as benign so they can be logged less severely by logging middleware.
  • Embeddable so you can extend functionality or write your own convenience wrappers

Installation

go get -u github.com/lobocv/simplerr

Error Codes

The following error codes are provided by default with simplerr. These can be extended by registering custom codes.

Error Code Description
Unknown The default code for errors that are not classified
AlreadyExists An attempt to create an entity failed because it already exists
NotFound Means some requested entity (e.g., file or directory) was not found
InvalidArgument The caller specified an invalid argument
MalformedRequest The syntax of the request cannot be interpreted (eg JSON decoding error)
Unauthenticated The request does not have valid authentication credentials for the operation.
PermissionDenied That the identity of the user is confirmed but they do not have permission to perform the request
ConstraintViolated A constraint in the system has been violated. Eg. a duplicate key error from a unique index
NotSupported The request is not supported
NotImplemented The request is not implemented
MissingParameter A required parameter is missing or empty
DeadlineExceeded A request exceeded it's deadline before completion
Canceled The request was canceled before completion
ResourceExhausted A limited resource, such as a rate limit or disk space, has been reached
Unavailable The server itself is unavailable for processing requests.

A complete list of standard error codes can be found here.

Custom Error Codes

Custom error codes can be registered globally with simplerr. The standard error codes cannot be overwritten and have reserved values from 0-99.

func main() {
    r := NewRegistry()
    r.RegisterErrorCode(100, "custom error description")
}
	

Basic usage

Creating errors

Errors can be created with New(format string, args... interface{}), which works similar to fmt.Errorf but instead returns a *SimplerError. You can then chain mutations onto the error to add additional information.

userID := 123
companyID := 456
err := simplerr.New("user %d does not exist in company %d", userID, companyID).
	Code(CodeNotFound).
	Aux("user_id", userID, "company_id", companyID)

In the above example, a new error is created and set to error code CodeNotFound. We have also attached auxiliary key-value pair information to the error that we can extract later on when we decide to handle or log the error.

Errors can also be wrapped with the Wrap(err error)) and Wrapf(err error, format string, args... []interface{}) functions:

func GetUser(userID int) (*User, error) {
    user, err := db.GetUser(userID)
    if err != nil {
        serr = simplerr.Wrapf(err, "failed to get user with id = %d", userID).
			Aux("user_id", userID)
        if errors.Is(err, sql.ErrNoRows) {
            serr.Code(CodeNotFound)   
        }
        return serr
    }
}

Attaching Custom Attributes to Errors

Simplerr lets you define and detect your own custom attributes on errors. This works similarly to the context package. An attribute is attached to an error using the Attr() mutator and can be retrieved using the GetAttribute() function, which finds the first match of the attribute key in the error chain.

It is highly recommended that a custom type be used as the key in order to prevent naming collisions of attributes. The following example defines a NotRetryable attribute and attaches it on an error where a unique constraint is violated, this indicates that the error should be exempt by any retry mechanism.

// Define a custom type so we don't get naming collisions for value == 1
type ErrorAttribute int

// Define a specific key for the attribute
const NotRetryable = ErrorAttribute(1)

// Attach the `NotRetryable` attribute on the error
serr := simplerr.New("user with that email already exists").
	Code(CodeConstraintViolated).
	Attr(NotRetryable, true)

// Get the value of the NotRetryable attribute
isRetryable, ok := simplerr.GetAttribute(err, NotRetryable).(bool)
// isRetryable == true

Detecting errors

SimpleError implements the Unwrap() method so it can be used with the standard library errors.Is() and errors.As() functions. However, the ability to use error codes makes abstracting and detecting errors much simpler. Instead of looking for a specific error, simplerr allows you to search for the kind of error by looking for an error code:

func GetUserSettings(userID int) (*Settings, error) {
    settings, err := db.GetSettings(userID)
    if err != nil {
        // If the settings do not exist, return defaults
        if simplerr.HasErrorCode(CodeNotFound) {
            return defaultSettings(), nil
        }
		
        serr := simplerr.Wrapf(err, "failed to get settings for user with id = %d", userID).
                         Aux("user_id", userID)
        return nil, serr
    }
	
    return settings, nil
}

The alternatives would be to use errors.Is(err, sql.ErrNoRows) directly and leak an implementation detail of the persistence layer or to define a custom error that the persistence layer would need to return in place of sql.ErrNoRows.

Error Handling

SimpleErrors were designed to be handled. The ecosystem package provides packages to assist with error handling for different applications. Designing your own handlers is as simple as detecting the SimpleError and reacting to it's attributes.

Detecting Errors

To detect a specific error code, you can use HasErrorCode(err error, c Code). If you want to look for several different error codes, use HasErrorCodes(err error, codes... Code), which returns the first of the provided error codes that is detected, and a boolean for whether anything was detected.

Logging SimpleErrors

One of the objective to simplerr is to reduce the need to log the errors manually at the sight in which they are raised, and instead, log errors in a procedural way in a middleware layer. While this is possible with standard library errors, there is a lack of control when dealing only with the simple string-backed error implementation.

Logging with Structured Loggers

It is good practice to use structured logging to improve observability. However, the standard library error does not allow for attaching and retrieving key-value pairs on errors. With simplerr you can retrieve a superset of all attached key-value data on errors in the chain using ExtractAuxiliary() or on the individual error with GetAuxiliary().

Benign Errors

Benign errors are errors that are mainly used to indicate a certain condition, rather than something going wrong in the system. An example of a benign error would be an API that returns sql.ErrNoRows when requesting a specific resource. Depending on whether the resource is expected to exist or not, this may not actually be an error.

Some clients may be calling the API to just check the existence of the resource. Nonetheless, this "error" would flood the logs at ERROR level and may disrupt error tracking tools such as sentry. The server must still return the error so that it reaches the client, however on the server, it is not seen as genuine error and does not need to be logged as such. With simplerr, it is possible to mark an error as benign, which allows logging middleware to detect and log the error at a less severe level such as INFO.

Errors can be marked benign by either using the Benign() or BenignReason() mutators. The latter also attaches a reason why the error was marked benign. To detect benign errors, use the IsBenign() function which looks for any benign errors in the chain of errors.

Silent Errors

Similar to benign errors, an error can be marked as silent using the Silence() mutator to indicate to logging middleware to not log this error at all. This is useful in situations where a very high amount of benign errors are flooding the logs. To detect silent errors, use the IsSilent() function which looks for any silent errors in the chain of errors.

Changing Error Formatting

The default formatting of the error string can be changed by modifying the simplerr.Formatter variable. For example, to use a new line to separate the message and the wrapped error you can do:

simplerr.Formatter = func(e *simplerr.SimpleError) string {
    parent := e.Unwrap()
    if parent == nil {
        return e.GetMessage()
    }
    return strings.Join([]string{e.GetMessage(), parent.Error()}, "\n")
}

HTTP Status Codes

HTTP status codes can be set automatically by using the ecosystem/http package to translate simplerr error codes to HTTP status codes. To do so, you must use the simplehttp.Handler or simplehttp.HandlerFuncinstead of the ones defined in the http package. The only difference between the two is that the simplehttp ones return an error. Adapters are provided in order to interface with the http package. These adapters call simplehttp.SetStatus() on the returned error in order to set the http status code on the response.

Given a server with the an endpoint GetUser:

func (s *Server) GetUser(resp http.ResponseWriter, req *http.Request) error {
	
    // extract userName from request...
	
    err := s.db.GetUser(userName)
	if err != nil {
		// This returned error is translated into a response code via the http adapter
	    return err
    }

    resp.WriteHeader(http.StatusCreated)
}

We can mount the endpoint with the simplehttp.NewHandlerAdapter():

s := &Server{}
http.ListenAndServe("", simplehttp.NewHandlerAdapter(s))

or if it was a handler function instead, using the simplehttp.NewHandlerFuncAdapter() method:

http.ListenAndServe("", simplehttp.NewHandlerFuncAdapter(fn))

Simplerr does not provide a 1:1 mapping of all HTTP status because there are too many obscure and under-utilized HTTP codes that would complicate and bloat the library. Most of the prevalent HTTP status codes have representation in simplerr. Additional translations can be added by registering a mapping:

func main() {
    m := simplehttp.DefaultMapping()
    m[simplerr.CodeCanceled] = http.StatusRequestTimeout
    simplehttp.SetMapping(m)
    // ...
}

GRPC Status Codes

Since GRPC functions return an error, it is even convenient to integrate error code translation using an interceptor (middleware). The package ecosystem/grpc defines an interceptor that detects if the returned error is a SimpleError and then translates the error code into a GRPC status code. A mapping for several codes is provided using the DefaultMapping() function. This can be changed by providing an alternative mapping when creating the interceptor:

func main() {
    // Get the default mapping provided by simplerr
    m := simplerr.DefaultMapping()
    // Add another mapping from simplerr code to GRPC code
    m[simplerr.CodeMalformedRequest] = codes.InvalidArgument
    // Create the interceptor by providing the mapping
    interceptor := simplerr.TranslateErrorCode(m)
}

Contributing

Contributions and pull requests to simplerr are welcome but must align with the goals of the package:

  • Keep it simple
  • Features should have reasonable defaults but provide flexibility with optional configuration
  • Keep dependencies to a minimum

Documentation

Index

Constants

View Source
const NumberOfReservedCodes = 100

NumberOfReservedCodes is the code number, under which, are reserved for use by this library.

Variables

View Source
var Formatter = DefaultFormatter

Formatter is the error string formatting function.

Functions

func DefaultFormatter added in v0.1.1

func DefaultFormatter(e *SimpleError) string

DefaultFormatter is the default error string formatting.

func ExtractAuxiliary added in v0.1.3

func ExtractAuxiliary(err error) map[string]interface{}

ExtractAuxiliary extracts a superset of auxiliary data from all errors in the chain. Wrapper error auxiliary data take precedent over later errors.

func GetAttribute added in v0.1.4

func GetAttribute(err error, key interface{}) (interface{}, bool)

GetAttribute gets the first instance of the key in the error chain. This can be used to define attributes on the error that do not have first-class support with simplerr. Much like keys in the `context` package, the `key` should be a custom type so it does not have naming collisions with other values.

func HasErrorCode

func HasErrorCode(err error, code Code) bool

HasErrorCode checks the error code of an error if it is a SimpleError{}. nil errors or errors that are not SimplErrors return false.

func IsBenign

func IsBenign(err error) (string, bool)

IsBenign checks the error or any error in the chain, is marked as benign. It also returns the reason the error was marked benign. Benign errors should be logged or handled less severely than non-benign errors. For example, you may choose to log benign errors at INFO level, rather than ERROR.

func IsSilent

func IsSilent(err error) bool

IsSilent checks the error or any error in the chain, is marked silent. Silent errors should not need to be logged at all.

func SetRegistry

func SetRegistry(r *Registry)

SetRegistry sets the default error registry

Types

type Call added in v0.1.1

type Call struct {
	Line int
	File string
	Func string
}

Call contains information for a specific call in the call stack

type Code

type Code int

Code is an error code that indicates the category of error

const (
	// CodeUnknown is the default code for errors that are not classified
	CodeUnknown Code = 0
	// CodeAlreadyExists means an attempt to create an entity failed because one
	// already exists.
	CodeAlreadyExists Code = 1
	// CodeNotFound means some requested entity (e.g., file or directory) was not found.
	CodeNotFound Code = 2
	// CodeInvalidArgument indicates that the caller specified an invalid argument.
	CodeInvalidArgument Code = 3
	// CodeMalformedRequest indicates the syntax of the request cannot be interpreted (eg JSON decoding error)
	CodeMalformedRequest Code = 4
	// CodeUnauthenticated indicates the request does not have valid authentication credentials for the operation.
	CodeUnauthenticated Code = 5
	// CodePermissionDenied indicates that the identity of the user is confirmed but they do not have permissions
	// to perform the request
	CodePermissionDenied Code = 6
	// CodeConstraintViolated indicates that a constraint in the system has been violated.
	// Eg. a duplicate key error from a unique index
	CodeConstraintViolated Code = 7
	// CodeNotSupported indicates that the request is not supported
	CodeNotSupported Code = 8
	// CodeNotImplemented indicates that the request is not implemented
	CodeNotImplemented Code = 9
	// CodeMissingParameter indicates that a required parameter is missing or empty
	CodeMissingParameter Code = 10
	// CodeDeadlineExceeded indicates that a request exceeded it's deadline before completion
	CodeDeadlineExceeded Code = 11
	// CodeCanceled indicates that the request was canceled before completion
	CodeCanceled Code = 12
	// CodeResourceExhausted indicates that some limited resource (eg rate limit or disk space) has been reached
	CodeResourceExhausted Code = 13
	// CodeUnavailable indicates that the server itself is unavailable for processing requests.
	CodeUnavailable Code = 14
)

These are common impact error codes that are found throughout our services

func HasErrorCodes added in v0.1.3

func HasErrorCodes(err error, codes ...Code) (Code, bool)

HasErrorCodes looks for the specified error codes in the chain of errors. It returns the first code in the list that is found in the chain and a boolean for whether anything was found.

type Registry

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

Registry is a registry of information on how to handle and serve simple errors

func GetRegistry added in v0.1.2

func GetRegistry() *Registry

GetRegistry gets the currently set registry

func NewRegistry

func NewRegistry() *Registry

NewRegistry creates a new registry without any defaults

func (*Registry) CodeDescription added in v0.1.1

func (r *Registry) CodeDescription(c Code) string

CodeDescription returns the description of the error code

func (*Registry) ErrorCodes

func (r *Registry) ErrorCodes() map[Code]string

ErrorCodes returns a copy of the registered error codes and their descriptions

func (*Registry) RegisterErrorCode added in v0.1.1

func (r *Registry) RegisterErrorCode(code Code, description string)

RegisterErrorCode registers custom error codes in the registry. This call will panic if the error code is already registered. Error codes 0-99 are reserved for simplerr. This method should be called early on application startup.

type SimpleError

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

SimpleError is an implementation of the `error` interface which provides functionality to ease in the operating and handling of errors in applications.

func As

func As(err error) *SimpleError

As attempts to find a SimpleError in the chain of errors, similar to errors.As(). Note that this will NOT match structs which embed a *SimpleError.

func New

func New(_fmt string, args ...interface{}) *SimpleError

New creates a new SimpleError from a formatted string

func Wrap

func Wrap(err error) *SimpleError

Wrap wraps the error in a SimpleError. It defaults the error code to CodeUnknown.

func Wrapf added in v0.1.1

func Wrapf(err error, msg string, a ...interface{}) *SimpleError

Wrapf returns a new SimpleError by wrapping an error with a formatted message string. It defaults the error code to CodeUnknown

func (*SimpleError) Attr added in v0.1.4

func (e *SimpleError) Attr(key, value interface{}) *SimpleError

Attr attaches an attribute to the error that can be detected when handling the error. Attr() behaves similarly to `context.WithValue()`. Keys should be custom types in order to avoid naming collisions. Use `GetAttribute()` to get the value of the attribute.

func (*SimpleError) Aux added in v0.1.1

func (e *SimpleError) Aux(kv ...interface{}) *SimpleError

Aux attaches auxiliary informational data to the error as key value pairs. All keys must be of type `string` and have a value. Keys without values are ignored. This auxiliary data can be retrieved by using `ExtractAuxiliary()` and attached to structured loggers. Do not use this to detect any attributes on the error, instead use Attr().`

func (*SimpleError) AuxMap added in v0.1.1

func (e *SimpleError) AuxMap(aux map[string]interface{}) *SimpleError

AuxMap attaches auxiliary informational data to the error from a map[string]interface{}. This auxiliary data can be retrieved by using `ExtractAuxiliary()` and attached to structured loggers. Do not use this to detect any attributes on the error, instead use Attr().`

func (*SimpleError) Benign

func (e *SimpleError) Benign() *SimpleError

Benign marks the error as "benign". A benign error is an error that depends on the context of the caller. eg a NotFoundError is only an error if the caller is expecting the entity to exist. These errors can usually be logged less severely (ie at INFO rather than ERROR level)

func (*SimpleError) BenignReason

func (e *SimpleError) BenignReason(reason string) *SimpleError

BenignReason marks the error as "benign" and attaches a reason it was marked benign. A benign error is an error depending on the context of the caller. eg a NotFoundError is only an error if the caller is expecting the entity to exist These errors can usually be logged less severely (ie at INFO rather than ERROR level)

func (*SimpleError) Code

func (e *SimpleError) Code(code Code) *SimpleError

Code sets the error code. The assigned code should be defined in the registry.

func (*SimpleError) Error

func (e *SimpleError) Error() string

Error satisfies the `error` interface. It uses the `simplerr.Formatter` to generate an error string.

func (*SimpleError) GetAttribute added in v0.1.9

func (e *SimpleError) GetAttribute(key interface{}) (interface{}, bool)

GetAttribute gets an attribute attached to this specific SimpleError. It does NOT traverse the error chain. This can be used to define attributes on the error that do not have first-class support with simplerr. Much like keys in the `context` package, the `key` should be a custom type so it does not have naming collisions with other values.

func (*SimpleError) GetAuxiliary added in v0.1.1

func (e *SimpleError) GetAuxiliary() map[string]interface{}

GetAuxiliary gets the auxiliary informational data attached to this error. This key-value data can be attached to structured loggers.

func (*SimpleError) GetBenignReason

func (e *SimpleError) GetBenignReason() (string, bool)

GetBenignReason returns the benign reason and whether the error was marked as benign ie. This error can be logged at INFO level and then discarded.

func (*SimpleError) GetCode

func (e *SimpleError) GetCode() Code

GetCode returns the error code as defined in the registry

func (*SimpleError) GetDescription added in v0.1.2

func (e *SimpleError) GetDescription() string

GetDescription returns the description of the error code on the error.

func (*SimpleError) GetMessage added in v0.1.1

func (e *SimpleError) GetMessage() string

GetMessage gets the error string for this error, exclusive of any wrapped errors.

func (*SimpleError) GetSilent

func (e *SimpleError) GetSilent() bool

GetSilent returns a flag that signals that this error should be recorded or logged silently on the server side ie. This error should not be logged at all

func (*SimpleError) Message added in v0.1.2

func (e *SimpleError) Message(msg string, args ...interface{}) *SimpleError

Message sets the message text on the error. This message it used to wrap the underlying error, if it exists.

func (*SimpleError) Silence

func (e *SimpleError) Silence() *SimpleError

Silence sets the error as silent. Silent errors can be ignored by loggers.

func (*SimpleError) StackTrace added in v0.1.1

func (e *SimpleError) StackTrace() []Call

StackTrace returns the stack trace at the point at which the error was raised.

func (*SimpleError) Unwrap

func (e *SimpleError) Unwrap() error

Unwrap implement the interface required for error unwrapping. It returns the underlying (wrapped) error.

Directories

Path Synopsis
ecosystem

Jump to

Keyboard shortcuts

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