Categorygithub.com/sbreitf1/errors
modulepackage
1.1.0
Repository: https://github.com/sbreitf1/errors.git
Documentation: pkg.go.dev

# README

Errors for GO

This library introduces an advanced error type for golang. It's advantages are better error comparison for error handling and automated test, as well as a built-in distinction between safe error messages to be passed to an end-user (e.g. API client) and technical details that can be written to a log file for further investigation.

Getting started

As this repository is compatible with go mod, it is sufficient to include the following package in your go code:

import "github.com/sbreitf1/errors"

Usage

The main error type of this package is Error which is also compatible with the commonly used error interface. Every instance of Error is typically generated from a globally defined template using Make():

import "github.com/sbreitf1/errors"

var (
    // template definition:
    ArgumentError = errors.New("Argument %s is not valid")
)

func FooBar(positiveValue int) errors.Error {
    if positiveValue < 0 {
        // instantiate error from template:
        return ArgumentError.Make().Args("positiveValue")
    }
    return nil
}

As can be seen in this example, templates can define format strings as consumed by fmt.Sprintf() that are evaluated by a later call to Args() supplying the content. You can also overwrite the whole message using Msg() on the generated error, but this will force the error message to be marked as unsafe.

A key element of this error type is the ability to define the safeness of error messages that specify which information can be displayed to API users without revealing critical secrets and implementation details. Call the Safe() mutator function after changing the error message via Msg() to allow printing the message in public contexts:

SafeArgumentError = errors.New("Argument %s is not valid").Safe()

Errors derived from this template will be safe and can be printed to public contexts. A call to Args() will maintain the safeness state as it only fills expected fields. Changing the message using Msg(), however, will remove the safeness-flag as stated above. A call to SafeString() will return the safe error message. If the error is not safe, a generic error message will be returned, including a unique id referring to this error instance. Printing the full error message Error() including stack trace and id to a log file allows for an indepth view without revealing details to the API client.

Comparison

Another advantage of this error package is the advanced and unified typing system. When creating a new error template using the global function New(ErrorType) you must specify a string denoting the error type of the new error. This error type string is used for comparison regardless of the actual message content. This allows for detailed error messages that can be compared using the same mechanisms as generic errors:

FileNotFoundError = errors.New("File %s not found")

err1 := FileNotFoundError.Make().Args("foo.txt")
err1.Is(FileNotFoundError) // => true, is instance of template FileNotFoundError

err2 := FileNotFoundError.Make().Args("bar.jpg");
err2.Equals(err1) // => true, different error messages but same type "FileNotFoundError"

Alternatively you can use the global functions AreEqual(error,error) and InstanceOf(error,Template) for checking in cases where the values might be nil:

InstanceOf(err1, FileNotFoundError) // => true
AreEqual(err1, err2) // => true

Logging and HTTP Responses

This package offers detailed logging and is compatible with the Gin framework for HTTP request handling. To use both functions in conjunction, you only need one call on a returned error object:

func handleRequest(c *gin.Context) {
    if err := someHandler(); err != nil {
        err.ToRequestAndLog(c)
        return
    }
}

The method ToRequestAndLog(RequestAborter, ...TypedError) executes both ToLog(...TypedError) and ToRequest(RequestAborter). The interface RequestAborter specifies the method AbortWithStatusJSON(int, interface{}) as used by the Gin framework for returning the given HTTP response code and JSON representation of a data object.

You can pass an arbitrary collection of errors and templates to ToLog(...TypedError) to specify which errors should be ignored. You may list functional errors here that should be reported to the API client but are not required in a log file. Furthermore, you can redirect logging by setting errors.Logger to an arbitrary function (string, ...interface{}) to write to a custom logger.

If you carefully maintain the error flags and error propagation in your application code, you won't need any conditions here as ToRequestAndLog will consider all parameters when printing the error message to log and request.

Mutator Functions

Mutator functions like Msg(), Args() and Safe() are used to change a specific property of the error. Every mutator function returns a new copy of Error allowing for a compact syntax. The following mutator functions are available on templates:

FunctionEffect
Track()Generate id for this error and print message to log (default)
Untrack()Disable automatic print to log. No id and stack trace will be generated for untracked errors
Trace()Allow stack traces for this error. This also marks the error template as tracked
NoTrace()Disallow stack traces for this error (default)
Safe()Set the safeness flag for this error
Msg(string, args...)Set the message for this error. If no args are supplied, the format string will be evaluated after a call to Args(args...)
HTTPCode(int)Sets the HTTP response code for this error
ErrCode(int)Sets the API error code for this error
API(int, int)A shortcut for .HTTPCode(int).ErrCode(int).Safe().Untrack() often used for functional API errors

Most of these methods are also available on errors. See the following list for a complete overview:

FunctionEffect
Untrack()Remove id and stack trace from this error and disable automatic log printing
NoTrace()Remove stack trace from this error
Safe()Set the safeness flag for this error
Msg(string, args...)Set the message for this error. If no args are supplied, the format string will be evaluated after a call to Args(args...)
Args(args...)Pass the format arguments for a previous call to Msg(string)
Cause(error)Saves a causing error as nested object in this error. Cause error strings will be appended to the error message
StrCause(string, args...)Generates a new generic error with message and appends it as cause
Expand(string, args...)Returns a copy of this error with the given error message and sets itself as cause
ExpandSafe(string, args...)Returns a copy of this error with the given error message with safeness-flag and sets itself as cause
HTTPCode(int)Sets the HTTP response code for this error
ErrCode(int)Sets the API error code for this error

Interopability

Log output of the errors package can be processed by any method that accepts parameters like fmt.Sprintf. If you are using Logrus you can simply use the Errorf function as logger:

errors.Logger = logrus.Errorf

By default all errors are printed directly to StdOut.

Best Practices

Error instantiation

Always use globally defined error templates for error instantiation. You may also define format messages without passing arguments to delay filling in the actual values when they are available in the application context. Use Make() during execution to instantiate a new error and prepare id and stack trace at this location:

var (
    DatabaseError = errors.New("Database unreachable")
    ElementNotFoundError = errors.New("Did not find resource %s").API(404, 0)
)

func example() {
    dbErr := DatabaseError.Make()
    queryErr := ElementNotFoundError.Make().Args("foobar")
}

Error propagation

Use Cause(error) to encapsulate a typical error object in the error model of this package:

var (
    ReadFileError = errors.New("Unable to read file %q")
)

function readData(file string) (string, errors.Error) {
    data, err := ioutil.ReadFile(file)
    if err != nil {
        return "", ReadFileError.Make().Args(file).Cause(err);
    }
    return string(data), nil
}

Then use Expand(string, args...) to propagate errors while maintaining the original error type:

function readResourceFile(relativePath string) (string, errors.Error) {
    data, err := readData(filepath.Join("data/resources", relativePath))
    if err != nil {
        return "", err.Expand("Could not read resource file")
    }
    return data, nil
}

Alternatively, you can use Cause(error) to propagate errors to semantically distinct steps:

var (
    ReadKeyError = errors.New("Unable to parse key")
)

function parseKey(file string) (*Key, errors.Error) {
    data, err := ioutil.ReadFile(file)
    if err != nil {
        return nil, ReadKeyError.Make().Cause(err)
    }
    [...]
}

For a fast way of propagating classical errors, you can use errors.Wrap that generates a new traced error using the input error type name:

function deleteFile(file string) (errors.Error) {
    return errors.Wrap(os.Remove(file))
}

As you can see, no special handling for <nil> is required here, as errors.Wrap will also return <nil> in this case.

TL;DR

TODO: short overview

# Functions

API returns a new APIError object.
AreEqual returns true if the type of both errors is the same regardless of the specific error message.
Assert performs test assertions to ensure error equality.
AssertNil performs test assertions to ensure the given error is nil.
DefaultAPI returns a new APIError object using the default http and error codes.
DefaultStdOutLogger prints all error messages to StdOut.
InstanceOf returns true if the given error is an instance of the given template.
New returns an error template and uses the message format string as error type.
ToRequest writes the given error to a HTTP request and returns true if err was not nil.
Wrap encapsulates any go-error in the extended Error type.
WrapT encapsulates any go-error in the extended Error type and appends the base error type to the message.

# Variables

ArgumentError denotes a missing or invalid argument.
ConfigurationError is an error that is caused by an invalid configuration.
GenericError represents a generic error with stack trace.
GenericSafeErrorMessage denotes the message replacement when exposing unsafe errors via API.
Logger is called to print errors and stack traces to log.
PrintUnsafeErrors controls wether unsafe (technical) error messages should be visible to the user in response messages.

# Structs

APIError represents a generic error repsonse object with code and message.
Template represents an error template that can be instatiated to an error using Make().

# Interfaces

Error is used as base error type in the whole application.
RequestAborter defines the required functionality to abort an HTTP request and is compatible with *gin.Context.
TypedError represents errors and templates that define an error type.

# Type aliases

ErrorType represents the base type of an error regardless of the specific error message.