errcode

package module
v0.3.0 Latest Latest
Warning

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

Go to latest
Published: Mar 13, 2019 License: Apache-2.0 Imports: 4 Imported by: 16

README

About Error codes

Error codes are particularly useful to reliably communicate the error type across program (network) boundaries. A program can reliably respond to an error if it can be sure to understand its type. Error monitoring systems can reliably understand what errors are occurring.

errcode overview

This package extends go errors via interfaces to have error codes. The two main goals are

  • The clients can reliably understand errors by checking against error codes.
  • Structure information is sent to the client

See the go docs for extensive API documentation.

There are other packages that add error code capabilities to errors. However, they almost universally use a single underlying error struct. A code or other annotation is a field on that struct. In most cases a structured error response is only possible to create dynamically via tags and labels.

errcode instead follows the model of pkg/errors to use wrapping and interfaces. You are encouraged to make your own error structures and then fulfill the ErrorCode interface by adding a function. Additional features (for example annotating the operation) are done via wrapping.

This design makes errcode highly extensible, inter-operable, and structure preserving (of the original error). It is easy to gradually introduce it into a project that is using pkg/errors (or the Cause interface).

Features

  • structured error representation
  • Follows the pkg/errors model where error enhancements are annotations via the Causer interface.
  • Internal errors show a stack trace but others don't.
  • Operation annotation. This concept is explained here.
  • Works for multiple errors when the Errors() interface is used. See the Combine function for constructing multiple error codes.
  • Extensible metadata. See how SetHTTPCode is implemented.
  • Integration with existing error codes
    • HTTP
    • GRPC (provided by separate grpc package)

Example

// First define a normal error type
type PathBlocked struct {
	start     uint64 `json:"start"`
	end       uint64 `json:"end"`
	obstacle  uint64 `json:"end"`
}

func (e PathBlocked) Error() string {
	return fmt.Sprintf("The path %d -> %d has obstacle %d", e.start, e.end, e.obstacle)
}

// Define a code. These often get re-used between different errors.
// Note that codes use a hierarchy to share metadata.
// This code is a child of the "state" code.
var PathBlockedCode = errcode.StateCode.Child("state.blocked")

// Now attach the code to your custom error type.
func (e PathBlocked) Code() Code {
	return PathBlockedCode
}

var _ ErrorCode = (*PathBlocked)(nil)  // assert implements the ErrorCode interface

Now lets see how you can send the error code to a client in a way that works with your exising code.

// Given just a type of error, give an error code to a client if it is present
if errCode := errcode.CodeChain(err); errCode != nil {
	// If it helps, you can set the code a header
	w.Header().Set("X-Error-Code", errCode.Code().CodeStr().String())

	// But the code will also be in the JSON body
	// Codes have HTTP codes attached as meta data.
	// You can also attach other kinds of meta data to codes.
	// Our error code inherits StatusBadRequest from its parent code "state"
	rd.JSON(w, errCode.Code().HTTPCode(), errcode.NewJSONFormat(errCode))
}

Let see a usage site. This example will include an annotation concept of "operation".

func moveX(start, end, obstacle) error {}
	op := errcode.Op("path.move.x")

	if start < obstable && obstacle < end  {
		return op.AddTo(PathBlocked{start, end, obstacle})
	}
}

Development

./scripts build
./scripts test
./scripts check

Documentation

Overview

Package errcode facilitates standardized API error codes. The goal is that clients can reliably understand errors by checking against immutable error codes

This godoc documents usage. For broader context, see https://github.com/pingcap/errcode/tree/master/README.md

Error codes are represented as strings by CodeStr (see CodeStr documentation).

This package is designed to have few opinions and be a starting point for how you want to do errors in your project. The main requirement is to satisfy the ErrorCode interface by attaching a Code to an Error. See the documentation of ErrorCode. Additional optional interfaces HasClientData, HasOperation, Causer, and StackTracer are provided for extensibility in creating structured error data representations.

Hierarchies are supported: a Code can point to a parent. This is used in the HTTPCode implementation to inherit HTTP codes found with MetaDataFromAncestors. The hierarchy is present in the Code's string representation with a dot separation.

A few generic top-level error codes are provided (see the variables section of the doc). You are encouraged to create your own error codes customized to your application rather than solely using generic errors.

See NewJSONFormat for an opinion on how to send back meta data about errors with the error data to a client. JSONFormat includes a body of response data (the "data field") that is by default the data from the Error serialized to JSON.

Stack traces are automatically added by NewInternalErr and show up as the Stack field in JSONFormat. Errors can be grouped with Combine() and ungrouped via Errors() which show up as the Others field in JSONFormat.

To extract any ErrorCodes from an error, use CodeChain(). This extracts error codes without information loss (using ChainContext).

Index

Constants

This section is empty.

Variables

View Source
var (
	// InternalCode is equivalent to HTTP 500 Internal Server Error.
	InternalCode = NewCode("internal").SetHTTP(http.StatusInternalServerError)

	// NotFoundCode is equivalent to HTTP 404 Not Found.
	NotFoundCode = NewCode("missing").SetHTTP(http.StatusNotFound)

	// UnimplementedCode is mapped to HTTP 501.
	UnimplementedCode = InternalCode.Child("internal.unimplemented").SetHTTP(http.StatusNotImplemented)

	// StateCode is an error that is invalid due to the current system state.
	// This operatiom could become valid if the system state changes
	// This is mapped to HTTP 400.
	StateCode = NewCode("state").SetHTTP(http.StatusBadRequest)

	// AlreadyExistsCode indicates an attempt to create an entity failed because it already exists.
	// This is mapped to HTTP 409.
	AlreadyExistsCode = StateCode.Child("state.exists").SetHTTP(http.StatusConflict)

	// OutOfRangeCode indicates an operation was attempted past a valid range.
	// This is mapped to HTTP 400.
	OutOfRangeCode = StateCode.Child("state.range")

	// InvalidInputCode is equivalent to HTTP 400 Bad Request.
	InvalidInputCode = NewCode("input").SetHTTP(http.StatusBadRequest)

	// AuthCode represents an authentication or authorization issue.
	AuthCode = NewCode("auth")

	// NotAuthenticatedCode indicates the user is not authenticated.
	// This is mapped to HTTP 401.
	// Note that HTTP 401 is poorly named "Unauthorized".
	NotAuthenticatedCode = AuthCode.Child("auth.unauthenticated").SetHTTP(http.StatusUnauthorized)

	// ForbiddenCode indicates the user is not authorized.
	// This is mapped to HTTP 403.
	ForbiddenCode = AuthCode.Child("auth.forbidden").SetHTTP(http.StatusForbidden)
)

Functions

func ClientData

func ClientData(errCode ErrorCode) interface{}

ClientData retrieves data from a structure that implements HasClientData If HasClientData is not defined it will use the given ErrorCode object. Normally this function is used rather than GetClientData.

func Operation

func Operation(v interface{}) string

Operation will return an operation string if it exists. It checks for the HasOperation interface. Otherwise it will return the zero value (empty) string.

func OperationClientData

func OperationClientData(errCode ErrorCode) (string, interface{})

OperationClientData gives the results of both the ClientData and Operation functions. The Operation function is applied to the original ErrorCode. If that does not return an operation, it is applied to the result of ClientData. This function is used by NewJSONFormat to fill JSONFormat.

func StackTrace

func StackTrace(err error) errors.StackTrace

StackTrace retrieves the errors.StackTrace from the error if it is present. If there is not StackTrace it will return nil

StackTrace looks to see if the error is a StackTracer or if a Causer of the error is a StackTracer. It will return the stack trace from the deepest error it can find.

Types

type AddOp

type AddOp func(ErrorCode) OpErrCode

AddOp is constructed by Op. It allows method chaining with AddTo.

func Op

func Op(operation string) AddOp

Op adds an operation to an ErrorCode with AddTo. This converts the error to the type OpErrCode.

op := errcode.Op("path.move.x")
if start < obstable && obstacle < end  {
	return op.AddTo(PathBlocked{start, end, obstacle})
}

func (AddOp) AddTo

func (addOp AddOp) AddTo(err ErrorCode) OpErrCode

AddTo adds the operation from Op to the ErrorCode

type Causer

type Causer interface {
	Cause() error
}

Causer allows the abstract retrieval of the underlying error. This is the interface that pkg/errors does not export but is considered part of the stable public API. TODO: export this from pkg/errors

Types that wrap errors should implement this to allow viewing of the underlying error. Generally you would use this via pkg/errors.Cause or pkg/errors.Unwrap.

type ChainContext

type ChainContext struct {
	Top     error
	ErrCode ErrorCode
}

ChainContext is returned by ErrorCodeChain to retain the full wrapped error message of the error chain. If you annotated an ErrorCode with additional information, it is retained in the Top field. The Top field is used for the Error() and Cause() methods.

func (ChainContext) Cause

func (err ChainContext) Cause() error

Cause satisfies the Causer interface

func (ChainContext) Code

func (err ChainContext) Code() Code

Code satisfies the ErrorCode interface

func (ChainContext) Error

func (err ChainContext) Error() string

Error satisfies the Error interface

func (ChainContext) Format

func (err ChainContext) Format(s fmt.State, verb rune)

Format implements the Formatter interface

func (ChainContext) GetClientData

func (err ChainContext) GetClientData() interface{}

GetClientData satisfies the HasClientData interface

type Code

type Code struct {
	Parent *Code
	// contains filtered or unexported fields
}

A Code has a CodeStr representation. It is attached to a Parent to find metadata from it.

func NewCode

func NewCode(codeRep CodeStr) Code

NewCode creates a new top-level code. A top-level code must not contain any dot separators: that will panic Most codes should be created from hierachry with the Child method.

func (Code) Child

func (code Code) Child(childStr CodeStr) Code

Child creates a new code from a parent. For documentation purposes, a childStr may include the parent codes with dot-separation. An incorrect parent reference in the string panics.

func (Code) CodeStr

func (code Code) CodeStr() CodeStr

CodeStr gives the full dot-separted path. This is what should be used for equality comparison.

func (Code) HTTPCode

func (code Code) HTTPCode() int

HTTPCode retrieves the HTTP code for a code or its first ancestor with an HTTP code. If none are specified, it defaults to 400 BadRequest

func (Code) IsAncestor

func (code Code) IsAncestor(ancestorCode Code) bool

IsAncestor looks for the given code in its ancestors.

func (Code) MetaDataFromAncestors

func (code Code) MetaDataFromAncestors(metaData MetaData) interface{}

MetaDataFromAncestors looks for meta data starting at the current code. If not found, it traverses up the hierarchy by looking for the first ancestor with the given metadata key. This is used in the HTTPCode implementation to inherit the HTTP Code from ancestors.

func (Code) SetHTTP

func (code Code) SetHTTP(httpCode int) Code

SetHTTP adds an HTTP code to the meta data. The code can be retrieved with HTTPCode. Panic if the metadata is already set for the code. Returns itself.

func (Code) SetMetaData

func (code Code) SetMetaData(metaData MetaData, item interface{}) error

SetMetaData is used to implement meta data setters such as SetHTTPCode. Return an error if the metadata is already set.

type CodeStr

type CodeStr string

CodeStr is the name of the error code. It is a representation of the type of a particular error. The underlying type is string rather than int. This enhances both extensibility (avoids merge conflicts) and user-friendliness. A CodeStr can have dot separators indicating a hierarchy.

Generally a CodeStr should never be modified once used by clients. Instead a new CodeStr should be created.

func (CodeStr) String

func (str CodeStr) String() string

type CodedError

type CodedError struct {
	GetCode Code
	Err     error
}

CodedError is a convenience to attach a code to an error and already satisfy the ErrorCode interface. If the error is a struct, that struct will get preseneted as data to the client.

To override the http code or the data representation or just for clearer documentation, you are encouraged to wrap CodeError with your own struct that inherits it. Look at the implementation of invalidInput, internalError, and notFound.

func NewCodedError

func NewCodedError(err error, code Code) CodedError

NewCodedError is for constructing broad error kinds (e.g. those representing HTTP codes) Which could have many different underlying go errors. Eventually you may want to give your go errors more specific codes. The second argument is the broad code.

If the error given is already an ErrorCode, that will be used as the code instead of the second argument.

func (CodedError) Cause

func (e CodedError) Cause() error

Cause satisfies the Causer interface.

func (CodedError) Code

func (e CodedError) Code() Code

Code returns the GetCode field

func (CodedError) Error

func (e CodedError) Error() string

func (CodedError) GetClientData

func (e CodedError) GetClientData() interface{}

GetClientData returns the underlying Err field.

type EmbedOp

type EmbedOp struct{ Op string }

EmbedOp is designed to be embedded into your existing error structs. It provides the HasOperation interface already, which can reduce your boilerplate.

func (EmbedOp) GetOperation

func (e EmbedOp) GetOperation() string

GetOperation satisfies the HasOperation interface

type ErrorCode

type ErrorCode interface {
	Error() string // The Error interface
	Code() Code
}

ErrorCode is the interface that ties an error and RegisteredCode together.

Note that there are additional interfaces (HasClientData, HasOperation, please see the docs) that can be defined by an ErrorCode to customize finding structured data for the client.

ErrorCode allows error codes to be defined without being forced to use a particular struct such as CodedError. CodedError is convenient for generic errors that wrap many different errors with similar codes. Please see the docs for CodedError. For an application specific error with a 1:1 mapping between a go error structure and a RegisteredCode, You probably want to use this interface directly. Example:

// First define a normal error type
type PathBlocked struct {
	start     uint64 `json:"start"`
	end       uint64 `json:"end"`
	obstacle  uint64 `json:"end"`
}

func (e PathBlocked) Error() string {
	return fmt.Sprintf("The path %d -> %d has obstacle %d", e.start, e.end, e.obstacle)
}

// Now define the code
var PathBlockedCode = errcode.StateCode.Child("state.blocked")

// Now attach the code to the error type
func (e PathBlocked) Code() Code {
	return PathBlockedCode
}

func CodeChain

func CodeChain(err error) ErrorCode

CodeChain resolves an error chain down to a chain of just error codes Any ErrorGroups found are converted to a MultiErrCode. Passed over error inforation is retained using ChainContext. If a code was overidden in the chain, it will show up as a MultiErrCode.

func ErrorCodes

func ErrorCodes(err error) []ErrorCode

ErrorCodes return all errors (from an ErrorGroup) that are of interface ErrorCode. It first calls the Errors function.

func NewForbiddenErr

func NewForbiddenErr(err error) ErrorCode

NewForbiddenErr creates a forbiddenErr from an err. If the error is already an ErrorCode it will use that code. Otherwise it will use ForbiddenCode which gives HTTP 401.

func NewInternalErr

func NewInternalErr(err error) ErrorCode

NewInternalErr creates an internalError from an err. If the given err is an ErrorCode that is a descendant of InternalCode, its code will be used. This ensures the intention of sending an HTTP 50x. This function also records a stack trace.

func NewInvalidInputErr

func NewInvalidInputErr(err error) ErrorCode

NewInvalidInputErr creates an invalidInput from an err. If the error is already an ErrorCode it will use that code. Otherwise it will use InvalidInputCode which gives HTTP 400.

func NewNotAuthenticatedErr

func NewNotAuthenticatedErr(err error) ErrorCode

NewNotAuthenticatedErr creates a notAuthenticatedErr from an err. If the error is already an ErrorCode it will use that code. Otherwise it will use NotAuthenticatedCode which gives HTTP 401.

func NewNotFoundErr

func NewNotFoundErr(err error) ErrorCode

NewNotFoundErr creates a notFound from an err. If the error is already an ErrorCode it will use that code. Otherwise it will use NotFoundCode which gives HTTP 404.

func NewUnimplementedErr

func NewUnimplementedErr(err error) ErrorCode

NewUnimplementedErr creates an internalError from an err. If the given err is an ErrorCode that is a descendant of InternalCode, its code will be used. This ensures the intention of sending an HTTP 50x. This function also records a stack trace.

type HasClientData

type HasClientData interface {
	GetClientData() interface{}
}

HasClientData is used to defined how to retrieve the data portion of an ErrorCode to be returned to the client. Otherwise the struct itself will be assumed to be all the data by the ClientData method. This is provided for exensibility, but may be unnecessary for you. Data should be retrieved with the ClientData method.

type HasOperation

type HasOperation interface {
	GetOperation() string
}

HasOperation is an interface to retrieve the operation that occurred during an error. The end goal is to be able to see a trace of operations in a distributed system to quickly have a good understanding of what occurred. Inspiration is taken from upspin error handling: https://commandcenter.blogspot.com/2017/12/error-handling-in-upspin.html The relationship to error codes is not one-to-one. A given error code can be triggered by multiple different operations, just as a given operation could result in multiple different error codes.

GetOperation is defined, but generally the operation should be retrieved with Operation(). Operation() will check if a HasOperation interface exists. As an alternative to defining this interface you can use an existing wrapper (OpErrCode via AddOp) or embedding (EmbedOp) that has already defined it.

type JSONFormat

type JSONFormat struct {
	Code      CodeStr           `json:"code"`
	Msg       string            `json:"msg"`
	Data      interface{}       `json:"data"`
	Operation string            `json:"operation,omitempty"`
	Stack     errors.StackTrace `json:"stack,omitempty"`
	Others    []JSONFormat      `json:"others,omitempty"`
}

JSONFormat is an opinion on how to serialize an ErrorCode to JSON. * Code is the error code string (CodeStr) * Msg is the string from Error() and should be friendly to end users. * Data is the ad-hoc data filled in by GetClientData and should be consumable by clients. * Operation is the high-level operation that was happening at the time of the error. The Operation field may be missing, and the Data field may be empty.

The rest of the fields may be populated sparsely depending on the application: * Stack is a stack trace. This is only given for internal errors. * Others gives other errors that occurred (perhaps due to parallel requests).

func NewJSONFormat

func NewJSONFormat(errCode ErrorCode) JSONFormat

NewJSONFormat turns an ErrorCode into a JSONFormat. If you use ErrorCodeChain first, you will ensure proper population of the Others field.

type MetaData

type MetaData map[CodeStr]interface{}

MetaData is used in a pattern for attaching meta data to codes and inheriting it from a parent. See MetaDataFromAncestors. This is used to attach an HTTP code to a Code as meta data.

type MultiErrCode

type MultiErrCode struct {
	ErrCode ErrorCode
	// contains filtered or unexported fields
}

A MultiErrCode contains at least one ErrorCode and uses that to satisfy the ErrorCode and related interfaces The Error method will produce a string of all the errors with a semi-colon separation. Later code (such as a JSON response) needs to look for the ErrorGroup interface.

func Combine

func Combine(initial ErrorCode, others ...ErrorCode) MultiErrCode

Combine constructs a MultiErrCode. It will combine any other MultiErrCode into just one MultiErrCode. This is "horizontal" composition. If you want normal "vertical" composition use BuildChain.

func (MultiErrCode) Cause

func (e MultiErrCode) Cause() error

Cause fullfills the Causer inteface

func (MultiErrCode) Code

func (e MultiErrCode) Code() Code

Code fullfills the ErrorCode inteface

func (MultiErrCode) Error

func (e MultiErrCode) Error() string

func (MultiErrCode) Errors

func (e MultiErrCode) Errors() []error

Errors fullfills the ErrorGroup inteface

func (MultiErrCode) Format

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

Format implements the Formatter interface

func (MultiErrCode) GetClientData

func (e MultiErrCode) GetClientData() interface{}

GetClientData fullfills the HasClientData inteface

type OpErrCode

type OpErrCode struct {
	Operation string
	Err       ErrorCode
}

OpErrCode is an ErrorCode with an Operation field attached. This can be conveniently constructed with Op() and AddTo() to record the operation information for the error. However, it isn't required to be used, see the HasOperation documentation for alternatives.

func (OpErrCode) Cause

func (e OpErrCode) Cause() error

Cause satisfies the Causer interface

func (OpErrCode) Code

func (e OpErrCode) Code() Code

Code returns the underlying Code of Err.

func (OpErrCode) Error

func (e OpErrCode) Error() string

Error prefixes the operation to the underlying Err Error.

func (OpErrCode) GetClientData

func (e OpErrCode) GetClientData() interface{}

GetClientData returns the ClientData of the underlying Err.

func (OpErrCode) GetOperation

func (e OpErrCode) GetOperation() string

GetOperation satisfies the HasOperation interface.

type StackCode

type StackCode struct {
	Err      ErrorCode
	GetStack errors.StackTracer
}

StackCode is an ErrorCode with stack trace information attached. This may be used as a convenience to record the strack trace information for the error. Generally stack traces aren't needed for user errors, but they are provided by NewInternalErr. Its also possible to define your own structures that satisfy the StackTracer interface.

func NewStackCode

func NewStackCode(err ErrorCode, position ...int) StackCode

NewStackCode constructs a StackCode, which is an ErrorCode with stack trace information The second variable is an optional stack position gets rid of information about function calls to construct the stack trace. It is defaulted to 1 to remove this function call.

NewStackCode first looks at the underlying error chain to see if it already has a StackTrace. If so, that StackTrace is used.

func (StackCode) Cause

func (e StackCode) Cause() error

Cause satisfies the Causer interface

func (StackCode) Code

func (e StackCode) Code() Code

Code returns the underlying Code of Err.

func (StackCode) Error

func (e StackCode) Error() string

Error ignores the stack and gives the underlying Err Error.

func (StackCode) GetClientData

func (e StackCode) GetClientData() interface{}

GetClientData returns the ClientData of the underlying Err.

func (StackCode) StackTrace

func (e StackCode) StackTrace() errors.StackTrace

StackTrace fulfills the StackTracer interface

Directories

Path Synopsis
Package grpc attaches GRPC codes to the standard error codes.
Package grpc attaches GRPC codes to the standard error codes.

Jump to

Keyboard shortcuts

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