Categorygithub.com/eluv-io/errors-go
modulepackage
1.0.3
Repository: https://github.com/eluv-io/errors-go.git
Documentation: pkg.go.dev

# README

Error Handling with eluv-io/errors-go

CodeQL

The package eluv-io/errors-go makes Go error handling simple, intuitive and effective.

err := someFunctionThatCanFail()
if err != nil {
    return errors.E("file download", errors.K.IO, err, "file", f, "user", usr)
}

The eluv-io/errors-go package promotes and facilitates these main design principles:

  • augment errors with relevant context information all along the call stack
  • provide this additional information in a structured form
  • provide call stack traces with minimal overhead if enabled through build tag and runtime config
  • integrate seamlessly with the structured logging convention of eluv-io/log

For sample code, see

Note that some of the unit tests and examples use the "old-style" explicit functions WithOp(op), WithKind(kind) and WithCause(err) functions to set values. They are not really needed anymore, and code can be simplified by using errors.E() exclusively. Furthermore, the use of templates makes the code even more compact in many situations - see sections below...

Creating & Wrapping Errors with E() and NoTrace()

The eluv-io/errors-go package provides one main function to create new or wrap existing errors with additional information:

// E creates a new error initialized with the given (optional) operation, kind, cause and key-value fields.
// All arguments are optional. If the first argument is a `string`, it is considered to be the operation.
//
// If no kind is specified, K.Other is assumed.
func E(args ...interface{}) *Error {...}

It's important to note that the first argument in E() - if it of type string - is the operation that returns the error, and not the error's reason. See the following examples:

// operation "file download" (op) failed with an "IO" error (kind) due to
// "err" (cause)
errors.E("file download", errors.K.IO, err)

// better than above with additional context information provided as key-value pairs
errors.E("file download", errors.K.IO, err, "file", f, "user", usr)

// sometimes there is no originating error (cause): use "reason" key-value
// pair to explain the cause
errors.E("validate configuration", errors.K.Invalid, 
	"reason", "part store location not specified")

// use "reason" for further clarification even if there is a cause,
// especially if the cause is a regular error (not an *errors.Error)
timeout, err := strconv.Atoi(timeoutString)
if err != nil {
	return errors.E("validate configuration", errors.K.Invalid, err,
		"reason", "failed to parse timeout",
		"timeout", timeoutString)
}

Note that supplementary context information (file & user in the example above) is provided as explicit key-value pairs instead of an opaque, custom-assembled string.

Use errors.NoTrace() instead of errors.E() to prevent recording and printing the stacktrace - see below for more information.

Besides the flexible E() function, the library's Error object also offers explicit methods for setting the different types of data:

errors.E().Op("file download").Kind(errors.K.IO).Cause(err).With("file", f).With("user", usr)

Since that code is just more verbose, the more compact E() function call with parameters should be preferred.

Reducing Code Complexity & Duplication with Template() and TemplateNoTrace

Template() or its shorter alias T() offers a great way to reduce code duplication in producing consistent errors. Imagine a validation function that checks multiple conditions and returns a corresponding error on any violations:

if len(path) == 0 {
    return errors.E("validation", errors.K.Invalid, "id", id, "reason", "no path")
}
if strings.ContainsAny(path, `$~\:`) {
    return errors.E("validation", errors.K.Invalid, "id", id, "reason", "contains illegal characters")
}
target, err := os.Readlink(path)
if err != nil {
    return errors.E("validation", errors.K.IO, err, "id", id, "reason", "not a link")
}

With a template function, this code can be rewritten in a more compact and concise form:

e := errors.Template("validation", errors.K.Invalid, "id", id)
if len(path) == 0 {
    return e("reason", "no path")
}
if strings.ContainsAny(path, `$~\:`) {
    return e("reason", "contains illegal characters")
}
target, err := os.Readlink(path)
if err != nil {
    return e(errors.K.IO, err, "reason", "not a link")
}

Use TemplateNoTrace() or its alias TNoTrace instead of Template() to prevent recording and printing the stacktrace in the generated error.

Use the template's IfNotNil() function to simplify error returns:

e := errors.Template(...)
err := someFunc(...)
return e.IfNotNil(err)

IfNotNil returns nil if err is nil and instantiates and *Error according to the template otherwise. It allows also to pass additional key-value pairs, e.g. e.IfNotNil(err, "key", "val")

Recording & Printing Call Stack Traces

Creating an error with E() or Template() per default also records the program counters of the current call stack, and Error() formats and prints the call stack:

op [getConfig] kind [invalid] cause:
	op [readFile] kind [I/O error] filename [illegal-filename|*] cause [open illegal-filename|*: no such file or directory]
	github.com/eluv-io/errors-go/stack_example_test.go:13 readFile()
	github.com/eluv-io/errors-go/stack_example_test.go:19 getConfig()
	github.com/eluv-io/errors-go/stack_example_test.go:21 getConfig()
	github.com/eluv-io/errors-go/stack_example_test.go:30 ExampleE()
	testing/run_example.go:64                             runExample()
	testing/example.go:44                                 runExamples()
	testing/testing.go:1505                               (*M).Run()
	_testmain.go:165                                      main()

Stack traces from multiple nested Errors are automatically coalesced into a single, comprehensive stack trace that is printed at the end.

The following global variables control stack trace handling at runtime:

  • PopulateStacktrace controls whether program counters are recorded when an Error is created
  • PrintStacktrace controls whether stack traces are printed in Error.Error()
  • PrintStacktracePretty controls the formatting of stack traces
  • MarshalStacktrace controls whether stack traces are marshalled to JSON

The following functions create an Error without stack trace:

  • errors.NoTrace()
  • errors.TemplateNoTrace()
  • errors.TNoTrace()

The following functions allow to suppress or remove stack traces from an Error:

  • errors.ClearStacktrace()
  • Error.FormatTrace()
  • Error.ClearStacktrace()

In addition, stack trace recording can be completely disabled at compile time with the errnostack build tag.

Error Lists with Append()

The Append() function allows collecting multiple errors in an error list and treat that list as a regular error:

func processBatch(items []work) error {
	var errs error
	for idx, item := range items {
		err := item.Execute()
		if err != nil {
			errs = errors.Append(errs, errors.E(err, "item", idx)
		}
	}
	return errs
}

errors.Append(list, err) appends a given error (or multiple errors) to an error list. The list itself can start out as nil or an initial regular error, in which case the function appends the errors to a new errors.List and returns the list.

An error list's string representation includes all appended errors:

error-list count [2]
    0: op [read] kind [I/O error] cause [EOF]
    1: op [write] kind [operation cancelled]

With stack traces:

error-list count [2]
    0: op [read] kind [I/O error] cause [EOF]
    github.com/eluv-io/errors-go/list_test.go:192 ExampleAppend()
    testing/run_example.go:64                     runExample()
    testing/example.go:44                         runExamples()
    testing/testing.go:1505                       (*M).Run()
    _testmain.go:165                              main()

    1: op [write] kind [operation cancelled]
    github.com/eluv-io/errors-go/list_test.go:192 ExampleAppend()
    testing/run_example.go:64                     runExample()
    testing/example.go:44                         runExamples()
    testing/testing.go:1505                       (*M).Run()
    _testmain.go:165                              main()

Integration with eluv-io/log-go

Thanks to providing all data as key-value pairs (including operation, kind and cause), the Error objects integrate seamlessly with the logging package whose main principle is that of structured logging. This means, for example, that errors are marshaled as JSON objects when the JSON handler is used for logging:

{
  "fields": {
    "error": {
      "cause": "EOF",
      "file": "/tmp/app-config.yaml",
      "kind": "I/O error",
      "op": "failed to parse config",
      "stacktrace": "runtime/asm_amd64.s:2337: runtime.goexit:\n\truntime/proc.go:195: ...main:\n\tsample/log_sample.go:66: main.main:\n\teluvio/log/sample/sub/sub.go:17: eluvio/log/sample/sub.Call"
    },
    "logger": "/eluvio/log/sample/sub"
  },
  "level": "warn",
  "timestamp": "2018-03-02T16:52:04.831737+01:00",
  "message": "failed to read config, using defaults"
}

Notice the "error" object: it is a JSON object with all data provided as fields. In addition, the error also contains a stacktrace when logged in JSON format.

# Functions

Append appends one or multiple errors `errs` to the *ErrorList err and returns the updated error list.
As is a convenience function that simply calls the stdlib errors.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.
ClearStacktrace removes the stacktrace from the given error if it's an instance of *Error.
E creates a new error initialized with the given (optional) operation, kind, cause and key-value fields.
Field returns the result of calling the Field() method on the given err if it is an *Error.
FromContext creates an error from the given context and additional error arguments as passed to E().
GetField returns the result of calling the GetField() method on the given err if it is an *Error.
GetRoot returns the innermost nested *Error of the given error, or nil if the provided object is not an *Error.
GetRootCause returns the first nested error that is not an *Error object.
Ignore simply ignores a potential error returned by the given function.
Is is a convenience function that simply calls the stdlib errors.Is() Is reports whether any error in err's chain matches target.
IsKind reports whether err is an *Error of the given Kind.
IsNotExist reports whether err is an *Error of Kind NotExist.
Match compares two errors.
NoTrace is the same as E, but does not populate a stack trace.
No description provided by the author
No description provided by the author
Str is an alias for the standard errors.New() function.
T is an alias for Template.
Template returns a function that creates a base error with an initial set of fields.
TemplateNoTrace is like Template but produces an error without stacktrace information.
TNoTrace is an alias for TemplateNoTrace.
TypeOf returns the type of the given value as string.
UnmarshalJsonErrorList unmarshals a list of errors.
Unwrap is a convenience function that simply calls the stdlib errors.Unwrap(): Unwrap returns the result of calling the Unwrap method on err, if err's type contains an Unwrap method returning error.
UnwrapAll returns the last non-nil error when repeatedly calling Unwrap on the given error.
Wrap wraps the given error in an Error instance with E(err) if err is not an *Error itself - otherwise returns err as *Error unchanged.

# Variables

DefaultFieldOrder defines the default order of an Error's fields in its String and JSON representations.
K defines the kinds of errors.
MarshalStacktrace controls whether stacktraces are marshaled to JSON or not.
MarshalStacktraceAsArray controls whether stacktraces are marshaled to JSON as a single string blob or as JSON array containing the individual lines of the stacktrace.
NilError is an error that represents a "nil" error value, but allows to call the Error() method without panicking.
PrintStacktrace controls whether stacktraces are printed per default or not.
PrintStacktracePretty enables additional formatting of stacktraces by aligning functions to the longest source filename.
Separator is the string used to separate nested errors.

# Structs

Error is the type that implements the error interface and which is returned by E(), NoTrace(), etc.
ErrorList is a collection of errors.

# Type aliases

DefaultKind is a Kind that can be set on an Error or Template that is only used if the kind is not otherwise set e.g.
Kind is the Go type for error kinds.
No description provided by the author
No description provided by the author