Documentation
¶
Overview ¶
Package errors is a general purpose error handling package, with few extra bells and whistles for HTTP interop.
Creating errors ¶
To facilitate error construction, the package provides a function, errors.E which build an *Error from its (functional) options.
func E(opt Option, opts ...Option) error
In typical use, calls to errors.E will arise multiple times within a method, so a constant called op could be defined that will be passed to all E calls within the method:
func (b *binder) Bind(r *http.Request, v interface{}) error { const op = "binder.Bind" if err := b.Decode(r, v); err != nil { return errors.E(errors.WithOp(op), errors.InvalidInput, errors.WithErr(err)) } if err := b.Validate.Struct(v); err != nil { return errors.E(errors.WithOp(op), errors.InvalidInput, errors.WithErr(err)) } return nil }
Adding context to an error ¶
A new error wrapping the original error is returned by passing it in to the constructor. Additional context can include the following (optional):
- errors.WithOp(string): Operation name being attempted.
- errors.Kind: Error classification.
- errors.WithText(string): Error string. errors.WithTextf(string, ...interface{}) can also be used to format the error string with additional arguments.
- errors.WithData(interface{}): Arbitrary value which could be considered relevant to the error.
Example:
if err := svc.SaveOrder(o); err != nil { return errors.E(errors.WithOp(op), errors.WithErr(err)) }
Inspecting errors ¶
The error Kind can be extracted using the function WhatKind(error):
if errors.WhatKind(err) == errors.NotFound { // ... }
With this, it is straightforward for the app to handle the error appropriately depending on the classification, such as a permission error or a timeout error.
There is also Match which can be useful in tests to compare and check only the properties which are of interest. This allows to easily ignore the irrelevant details of the error.
The function checks whether the error is of type *Error, and if so, whether the fields within equal those within the template. The key is that it checks only those fields that are non-zero in the template, ignoring the rest.
if errors.Match(errors.E(errors.WithOp("service.MakeBooking"), errors.PermissionDenied), err) { // ... }
Errors for the end user ¶
Errors have multiple consumers, the end user being one of them. However, to the end user, the error text, root cause etc. are not relevant (and in most cases, should not be exposed as it could be a security concern).
To address this, Error has the field UserMsg which is intended to be returned/shown to the user. WithUserMsg(string) stores the message into the aforementioned field. And it can be retrieved using errors.UserMsg.
Example:
// CreateUser creates a new user in the system. func (s *Service) CreateUser(ctx context.Context, user *myapp.User) error { const op = "svc.CreateUseer" // Validate username is non-blank. if user.Username == "" { msg := "Username is required" return errors.E(errors.WithOp(op), errors.InvalidInput, errors.WithUserMsg(msg)) } // Verify user does not already exist if s.usernameInUse(user.Username) { msg := "Username is already in use. Please choose a different username." return errors.E(errors.WithOp(op), errors.AlreadyExists, errors.WithUserMsg(msg)) } // ... } // Elsewhere in the application, for responding with the error to the user if msg := errors.UserMsg(err); msg != "" { // ... }
Index ¶
- Variables
- func As(err error, target interface{}) bool
- func Diff(template, err error) []string
- func E(opt Option, opts ...Option) error
- func Is(err, target error) bool
- func Join(errs ...error) error
- func Match(template, err error) bool
- func New(text string) error
- func StatusCode(err error) int
- func Unwrap(err error) error
- func UserMsg(err error) string
- type Error
- type Fields
- type GetKind
- type InternalDetails
- type JSONFunc
- type Kind
- type Option
- func Options(opts ...Option) Option
- func WithData(data interface{}) Option
- func WithErr(err error) Option
- func WithOp(op string) Option
- func WithResp(resp *http.Response) Option
- func WithText(text string) Option
- func WithTextf(format string, args ...interface{}) Option
- func WithToJSON(f JSONFunc) Option
- func WithUserMsg(msg string) Option
- type OptionFunc
- type StatusCoder
Examples ¶
Constants ¶
This section is empty.
Variables ¶
var ( // Unknown error. An example of where this error may be returned is // if a Status value received from another address space belongs to // an error-space that is not known in this address space. Also // errors raised by APIs that do not return enough error information // may be converted to this error. Unknown = Kind{} // InvalidInput indicates client specified an invalid input. // Note that this differs from FailedPrecondition. It indicates arguments // that are problematic regardless of the state of the system // (e.g., a malformed file name). InvalidInput = Kind{ Code: "INVALID_INPUT", Status: http.StatusBadRequest, } // Unauthenticated indicates the request does not have valid // authentication credentials for the operation. Unauthenticated = Kind{ Code: "UNAUTHENTICATED", Status: http.StatusUnauthorized, } // PermissionDenied indicates the caller does not have permission to // execute the specified operation. It must not be used for rejections // caused by exhausting some resource (use ResourceExhausted instead // for those errors). It must not be used if the caller cannot be // identified (use Unauthenticated instead for those errors). PermissionDenied = Kind{ Code: "PERMISSION_DENIED", Status: http.StatusForbidden, } // NotFound means some requested entity (e.g., file or directory) was // not found. NotFound = Kind{ Code: "NOT_FOUND", Status: http.StatusNotFound, } // Conflict indicates the request conflicts with the current state // of the server. Conflict = Kind{ Code: "CONFLICT", Status: http.StatusConflict, } // FailedPrecondition indicates operation was rejected because the // system is not in a state required for the operation's execution. // For example, directory to be deleted may be non-empty, an rmdir // operation is applied to a non-directory, etc. // // A litmus test that may help a service implementor in deciding // between FailedPrecondition and Unavailable: // (a) Use Unavailable if the client can retry just the failing call. // (b) Use FailedPrecondition if the client should not retry until // the system state has been explicitly fixed. E.g., if an "rmdir" // fails because the directory is non-empty, FailedPrecondition // should be returned since the client should not retry unless // they have first fixed up the directory by deleting files from it. // (c) Use FailedPrecondition if the client performs conditional // REST Get/Update/Delete on a resource and the resource on the // server does not match the condition. E.g., conflicting // read-modify-write on the same resource. FailedPrecondition = Kind{ Code: "FAILED_PRECONDITION", Status: http.StatusPreconditionFailed, } // ResourceExhausted indicates some resource has been exhausted, perhaps // a per-user quota, or perhaps the entire file system is out of space. ResourceExhausted = Kind{ Code: "RESOURCE_EXHAUSTED", Status: http.StatusTooManyRequests, } // Internal errors. Means some invariants expected by underlying // system has been broken. If you see one of these errors, // something is very broken. Internal = Kind{ Code: "INTERNAL", Status: http.StatusInternalServerError, } // Canceled indicates the operation was canceled (typically by the caller). Canceled = Kind{ Code: "CANCELED", Status: http.StatusInternalServerError, } // Unimplemented indicates operation is not implemented or not // supported/enabled in this service. Unimplemented = Kind{ Code: "UNIMPLEMENTED", Status: http.StatusNotImplemented, } // This is a most likely a transient condition and may be corrected // by retrying with a backoff. Note that it is not always safe to retry // non-idempotent operations. // // See litmus test above for deciding between FailedPrecondition and // Unavailable. Unavailable = Kind{ Code: "UNAVAILABLE", Status: http.StatusServiceUnavailable, } // DeadlineExceeded means operation expired before completion. // For operations that change the state of the system, this error may be // returned even if the operation has completed successfully. For // example, a successful response from a server could have been delayed // long enough for the deadline to expire. DeadlineExceeded = Kind{ Code: "DEADLINE_EXCEEDED", Status: http.StatusServiceUnavailable, } )
Functions ¶
func As ¶
As finds the first error in err's chain that matches target, and if so, sets target to that error value and returns true. Otherwise, it returns false.
The chain consists of err itself followed by the sequence of errors obtained by repeatedly calling Unwrap.
An error matches target if the error's concrete value is assignable to the value pointed to by target, or if the error has a method As(interface{}) bool such that As(target) returns true. In the latter case, the As method is responsible for setting target.
An error type might provide an As method so it can be treated as if it were a a different error type.
As panics if target is not a non-nil pointer to either a type that implements error, or to any interface type.
func Diff ¶
Diff compares and returns the diff between two error arguments. It can be used to extract the exact details of difference between expected and got errors in tests. It returns an empty array iff both arguments are of type *Error and every non-zero field of the template error is equal to the corresponding field of the second. If the Err field is a *Error, Diff recurs on that field; otherwise it compares the strings returned by the Error methods. Elements that are in the second argument but not present in the template are ignored.
For example,
errors.Diff(errors.E(errors.WithOp("service.MakeBooking"), errors.PermissionDenied), err)
tests whether err is an Error with Kind=PermissionDenied and Op=service.MakeBooking.
func E ¶
E builds an error value with the provided options.
if err := svc.ProcessSomething(); err != nil { return errors.E(errors.WithOp(op), errors.WithErr(err)) }
Example ¶
package main import ( "fmt" "github.com/sudo-suhas/xgo/errors" ) func main() { // Simple error e1 := errors.E( errors.WithOp("xgo_test.SimpleError"), errors.Internal, errors.WithText("fail"), ) fmt.Println("\nSimple error:") fmt.Println(e1) // Nested error. e2 := errors.E(errors.WithOp("xgo_test.NestedError"), errors.WithErr(e1)) fmt.Println("\nNested error:") fmt.Println(e2) }
Output: Simple error: xgo_test.SimpleError: internal error: fail Nested error: xgo_test.NestedError: internal error: xgo_test.SimpleError: fail
func Is ¶
Is reports whether any error in err's chain matches target.
The chain consists of err itself followed by the sequence of errors obtained by repeatedly calling Unwrap.
An error is considered to match a target if it is equal to that target or if it implements a method Is(error) bool such that Is(target) returns true.
An error type might provide an Is method so it can be treated as equivalent to an existing error. For example, if MyError defines
func (m MyError) Is(target error) bool { return target == os.ErrExist }
then Is(MyError{}, os.ErrExist) returns true. See syscall.Errno.Is for an example in the standard library.
func Join ¶ added in v0.2.1
Join returns an error that wraps the given errors. Any nil error values are discarded. Join returns nil if errs contains no non-nil values. The error formats as the concatenation of the strings obtained by calling the Error method of each element of errs, with a newline between each string.
func Match ¶
Match compares its two error arguments. It can be used to check for expected errors in tests. Both arguments must have underlying type *Error or Match will return false. Otherwise, it returns true iff every non-zero field of the template error is equal to the corresponding field of the error being checked. If the Err field is a *Error, Match recurs on that field; otherwise it compares the strings returned by the Error methods. Elements that are in the second argument but not present in the first are ignored.
For example,
errors.Match(errors.E(errors.WithOp("service.MakeBooking"), errors.PermissionDenied), err)
tests whether err is an Error with Kind=PermissionDenied and Op=service.MakeBooking.
Example ¶
package main import ( "fmt" "github.com/sudo-suhas/xgo/errors" ) func main() { msg := "Oops! Something went wrong. Please try again after some time." err := errors.New("network unreachable") // Construct an error, one we pretend to have received from a test. got := errors.E( errors.WithOp("Get"), errors.WithUserMsg(msg), errors.Unavailable, errors.WithErr(err), ) // Now construct a reference error, which might not have all // the fields of the error from the test. expect := errors.E( errors.WithOp("Get"), errors.Unavailable, errors.WithErr(err), ) fmt.Println("Match:", errors.Match(expect, got)) // Now one that's incorrect - wrong Kind. got = errors.E( errors.WithOp("Get"), errors.WithUserMsg(msg), errors.PermissionDenied, errors.WithErr(err), ) fmt.Println("Mismatch:", errors.Match(expect, got)) }
Output: Match: true Mismatch: false
func New ¶
New returns an error that formats as the given text. Each call to New returns a distinct error value even if the text is identical.
func StatusCode ¶
StatusCode attempts to determine the HTTP status code which is suitable for the error response. If the error does not implement StatusCoder or if it lacks type/classification info, http.StatusInternalServerError is returned. This applies for `nil` error as well and this case should be guarded with a nil check at the caller side.
Example ¶
package main import ( "database/sql" "fmt" "net/http" "github.com/sudo-suhas/xgo/errors" ) func main() { // Somewhere in the application, return the error with Kind: NotFound. err := errors.E(errors.WithOp("Get"), errors.NotFound, errors.WithErr(sql.ErrNoRows)) // Use StatusCode to extract the status code associated with the error Kind. fmt.Println("Status code:", errors.StatusCode(err)) // If required, define and use a custom Kind k := errors.Kind{Code: "CONFLICT", Status: http.StatusConflict} err = errors.E(errors.WithOp("UpdateEntity"), k) fmt.Println("Status code with custom kind:", errors.StatusCode(err)) }
Output: Status code: 404 Status code with custom kind: 409
func Unwrap ¶
Unwrap returns the result of calling the Unwrap method on err, if err's type contains an Unwrap method returning error. Otherwise, Unwrap returns nil.
func UserMsg ¶
UserMsg returns the first message suitable to be shown to the end-user in the error chain.
Example ¶
package main import ( "fmt" "github.com/sudo-suhas/xgo/errors" ) func main() { // Along with the error Kind, associate the appropriate message for the // specific error scenario. msg := "Username is required" e1 := errors.E(errors.WithOp("svc.CreateUser"), errors.InvalidInput, errors.WithUserMsg(msg)) // Use UserMsg to extract the message to be shown to the user. fmt.Println("User message:", errors.UserMsg(e1)) // Override the message by wrapping it in a new error with a different // message. e2 := errors.E(errors.WithErr(e1), errors.WithUserMsg("Deal with it!")) fmt.Println("Overidden user message:", errors.UserMsg(e2)) }
Output: User message: Username is required Overidden user message: Deal with it!
Types ¶
type Error ¶
type Error struct { // Op describes an operation, usually as the package and method, // such as "key/server.Lookup". Op string // Kind is the class of error, such as permission failure, // or "Unknown" if its class is unknown or irrelevant. Kind Kind // Text is the error string. Text is not expected to be suitable to // be shown to the end user. Text string // UserMsg is the error message suitable to be shown to the end // user. UserMsg string // Data is arbitrary value associated with the error. Data is not // expected to be suitable to be shown to the end user. Data interface{} // Err is the underlying error that triggered this one, if any. Err error // ToJSON is used to override the default implementation of // converting the Error instance into a JSON value. Optional. ToJSON JSONFunc }
Error is the type that implements the error interface. An Error value may leave some values unset.
If the error is printed, only those items that have been set to non-zero values will appear in the result.
If Kind is not specified or Unknown, we try to set it to the Kind of the underlying error.
func (*Error) Details ¶
func (e *Error) Details() InternalDetails
Details constructs and yields the details of the error by traversing the error chain.
func (*Error) JSON ¶
func (e *Error) JSON() interface{}
JSON is the default implementation of representing the error as a JSON value.
func (*Error) StatusCode ¶
StatusCode attempts to determine the HTTP status code which is suitable for the error Kind.
type Fields ¶
type Fields Error
Fields sets the fields specified on the Error instance. All fields are optional but at least 1 must be specified. Zero values are ignored.
type GetKind ¶
type GetKind interface {
GetKind() Kind
}
GetKind is implemented by any value that has a GetKind method. The method is used to determine the type or classification of error.
type InternalDetails ¶
type InternalDetails struct { Ops []string `json:"ops,omitempty"` Kind Kind `json:"kind,omitempty"` Error string `json:"error"` Data interface{} `json:"data,omitempty"` }
InternalDetails is the internal details populated from the error instance.
type JSONFunc ¶
type JSONFunc func(*Error) interface{}
JSONFunc converts the Error instance to a JSON value. It is recommended to return one of the following:
- map[string]interface{}
- []map[string]inteface{}
- CustomType
In certain cases, such as validation errors, it is reasonable to expand a single error instance into multiple 'Objects'.
Care must be taken not to expose details internal to the application. This is for two reasons, namely security and user experience.
type Kind ¶
Kind is the type of error. It is the tuple of the error code and HTTP status code. Defining custom Kinds in application domain is recommended if the predeclared Kinds are not suitable.
func KindFromCode ¶
KindFromCode returns the error kind based on the given error code.
It is not aware of any Kind defined in the application domain.
func KindFromStatus ¶
KindFromStatus returns the Kind based on the given HTTP status code.
It is not aware of any Kind defined in the application domain.
func WhatKind ¶
WhatKind returns the Kind associated with the given error. If the error is nil or does not implement GetKind interface, Unknown is returned.
Example ¶
package main import ( "database/sql" "fmt" "github.com/sudo-suhas/xgo/errors" ) func main() { // Somewhere in the application, return the error with Kind: NotFound. err := errors.E(errors.WithOp("Get"), errors.NotFound, errors.WithErr(sql.ErrNoRows)) // Use WhatKind to extract the Kind associated with the error. fmt.Println("Kind:", errors.WhatKind(err)) // If required, define and use a custom Kind k := errors.Kind{Code: "CONFLICT"} err = errors.E(errors.WithOp("UpdateEntity"), k) fmt.Println("Custom kind:", errors.WhatKind(err)) }
Output: Kind: not found Custom kind: conflict
type Option ¶
type Option interface {
Apply(*Error)
}
Option is a type of constructor option for E(...)
func WithData ¶
func WithData(data interface{}) Option
WithData sets the Data on the Error instance. It can be any arbitrary value associated with the error.
func WithOp ¶
WithOp sets the Op on the Error instance.
op describes an operation, usually as the package and method, such as "key/server.Lookup".
func WithResp ¶
WithResp sets the Text, Kind, Data on the Error instance.
HTTP method combined with the request path and the response status is set as the Text. It is not recommended to set the request path as the Op since this URL can include an ID for some entity.
The response status code is interpolated to the Kind using KindFromStatus.
The response body is set as the Data. Special handling is included for detecting and preserving JSON response.
func WithText ¶
WithText sets the Text on the Error instance. It is treated as an error message and should not be exposed to the end user.
func WithTextf ¶
WithTextf sets the formatted Text on the Error instance. It is treated as an error message and should not be exposed to the end user.
func WithToJSON ¶
WithToJSON sets ToJSON on the Error instance. It defines the conversion of Error instance to a JSON value.
func WithUserMsg ¶
WithUserMsg sets the UserMsg on the Error instance. msg should be suitable to be exposed to the end user.
type OptionFunc ¶
type OptionFunc func(*Error)
OptionFunc type is an adapter to allow the use of ordinary functions as error constructor options. If f is a function with the appropriate signature, OptionFunc(f) is an Option that calls f.
type StatusCoder ¶
type StatusCoder interface {
StatusCode() int
}
StatusCoder is implemented by any value that has a StatusCode method. The method is used to map the type or classification of error to the HTTP status code for the response from server.