terr

package module
v1.0.16 Latest Latest
Warning

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

Go to latest
Published: Feb 10, 2024 License: MIT Imports: 4 Imported by: 0

README

terr

Go Reference Test workflow

terr (short for traced error) is a minimalistic library for adding error tracing to Go 1.20+.

The error representation primitives introduced in Go 1.13 are quite sufficient, but the lack of tracing capabilities makes it hard to confidently debug errors across layers in complex applications.

To help with that, terr fully embraces the native Go error handling paradigms, but it adds two features:

  • file and line information for tracing errors;
  • the ability to print error tracing trees using the fmt package with the %@ verb.

This package introduces the concept of traced errors: a wrapper for errors that includes the location where they were created (errors.New), passed along (return err), wrapped (%w) or masked (%v). Traced errors keep track of children traced errors that relate to them. An error is a traced error if it was returned by one of the functions of this package.

Most importantly, traced errors work seamlessly with errors.Is, errors.As and errors.Unwrap just as if terr were not being used.

This blog post explains more about good practices for error handling in Go and the reasons for terr being created.

Using terr

Without terr With terr
errors.New("error") terr.Newf("error")
fmt.Errorf("error: %w", err) terr.Newf("error: %w", err)
[return] err terr.Trace(err)
[return] &CustomError{} terr.TraceSkip(&CustomError{}, 1)

terr.Newf can receive multiple errors. In fact, it is just a very slim wrapper around fmt.Errorf. Any traced error passed to terr.Newf will be included in the error tracing tree, regardless of the fmt verb used for it.

terr.Trace and terr.TraceSkip on the other hand do nothing with the error they receive (no wrapping and no masking), but they add one level to the error tracing tree. terr.TraceSkip lets custom errors constructors return a traced error with the location defined by skipping a number of stack frames.

To obtain the full trace, terr functions must be used consistently. If fmt.Errorf is used at one point, the error tracing information will be reset at that point, but Go's wrapped error tree will be preserved even in that case.

To see these functions in use, check out the package examples.

In the glorious day error tracing is added to Go, and assuming it gets done in a way that respects error handling as defined in Go 1.13+, removing terr from a code base should be a matter of replacing the terr function calls with equivalent expressions.

Printing errors

An error tracing tree can be printed with the special %@ formatting verb (example).

%@ prints the tree in a tab-indented, multi-line representation. If a custom format is needed (e.g., JSON), it is possible to implement a function that walks the error tracing tree and outputs it in the desired format. See how to walk the error tracing tree.

Tracing custom errors

Constructor functions for custom error types and wrapped sentinel errors can use terr.TraceSkip(err, skip) (example).

Walking the error tracing tree

Starting with Go 1.20, wrapped errors are kept as a n-ary tree. terr works by building a tree containing tracing information in parallel, leaving the Go error tree untouched, as if terr were not being used. Each traced error is thus a node in this parallel error tracing tree.

terr.TraceTree(err) ErrorTracer can be used to obtain the root of an n-ary error tracing tree, which can be navigated using the following methods:

type ErrorTracer interface {
	Error() string
	Location() (string, int)
	Children() []ErrorTracer
}

Note that this is not the tree of wrapped errors built by the Go standard library, because:

  • if non-traced errors are provided to terr.Newf, even if wrapped, they will not be a part of the error tracing tree;
  • even masked (%v) errors will be part of the error tracing tree if terr.Newf was used to mask them.

Methods provided by the by the Go standard library should be used to walk Go's wrapped error tree, which would include non-traced errors and ignore masked errors (e.g., errors.Unwrap).

An example is available.

Adopting terr

Adopting terr requires some thought about how errors are being constructed and which errors are worth tracing. Usage of terr may vary greatly for different code bases, and it is easiest to adopt terr when just the vanilla Go error handling practices are in use, without any third-party libraries.

The terr examples, particularly the example for terr.TraceSkip, provide a good illustration of how to use terr while following Go's recommended error guidelines.

In larger code bases, using gofmt -r might help, but it might also produce unwanted results if not used carefully. Applying the reverse of the rewrite rules for getting rid of terr may be helpful when introducing terr to a code base.

Getting rid of terr

While adding terr to a large code base can take some effort, removing it is very easy. Run the following commands in a directory tree to get rid of terr in all its files (make sure goimports is installed first):

$ gofmt -w -r 'terr.Newf(a) -> errors.New(a)' .
$ gofmt -w -r 'terr.Newf -> fmt.Errorf' .
$ gofmt -w -r 'terr.Trace(a) -> a' .
$ gofmt -w -r 'terr.TraceSkip(a, b) -> a' .
$ goimports -w .
$ go mod tidy

Also make sure to remove any uses of the %@ fmt verb, since this verb only works with traced errors.

Documentation

Overview

Package terr implements a set of functions for tracing errors in Go 1.20+.

Example

This example shows how to combine different terr functions and print an error tracing tree at the end.

package main

import (
	"fmt"

	"github.com/alnvdl/terr"
)

func main() {
	err := terr.Newf("base")
	traced := terr.Trace(err)
	wrapped := terr.Newf("wrapped: %w", traced)
	masked := terr.Newf("masked: %v", wrapped)
	fmt.Printf("%@\n", masked)
}
Output:

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func Newf

func Newf(format string, a ...any) error

Newf works exactly like fmt.Errorf, but returns a traced error. All traced errors passed as formatting arguments are included as children, regardless of the formatting verbs used for these errors. This function is equivalent to fmt.Errorf("...", ...). If used without verbs and additional arguments, it is equivalent to errors.New("...").

Example

This example shows how Newf interacts with traced and non-traced errors. Traced errors are included in the trace regardless of the fmt verb used for them, while non-traced errors are handled as fmt.Errorf would, but they do not get included in the trace.

package main

import (
	"errors"
	"fmt"

	"github.com/alnvdl/terr"
)

func main() {
	nonTracedErr := errors.New("non-traced")
	tracedErr1 := terr.Newf("traced 1")
	tracedErr2 := terr.Newf("traced 2")
	newErr := terr.Newf("errors: %w, %v, %w",
		nonTracedErr,
		tracedErr1,
		tracedErr2)

	fmt.Printf("%@\n", newErr)
	fmt.Println("---")

	// errors.Is works.
	fmt.Println("newErr is nonTracedErr:", errors.Is(newErr, nonTracedErr))
	fmt.Println("newErr is tracedErr1:", errors.Is(newErr, tracedErr1))
	fmt.Println("newErr is tracedErr2:", errors.Is(newErr, tracedErr2))
}
Output:

func Trace

func Trace(err error) error

Trace returns a new traced error for err. If err is already a traced error, a new traced error will be returned containing err as a child traced error. No wrapping or masking takes place in this function.

Example

This example shows how Trace interacts with traced and non-traced errors.

package main

import (
	"errors"
	"fmt"

	"github.com/alnvdl/terr"
)

func main() {
	// Adds tracing information to non-traced errors.
	nonTracedErr := errors.New("non-traced")
	fmt.Printf("%@\n", terr.Trace(nonTracedErr))
	fmt.Println("---")
	// Adds another level of tracing information to traced errors.
	tracedErr := terr.Newf("traced")
	fmt.Printf("%@\n", terr.Trace(tracedErr))
}
Output:

func TraceSkip added in v1.0.14

func TraceSkip(err error, skip int) error

TraceSkip works exactly like Trace, but lets the caller skip a number of stack frames when detecting the error location, with 0 identifying the caller of TraceSkip. This function can be used in custom error constructor functions, so they can return a traced error pointing at their callers.

Example

This example shows how to use TraceSkip to add tracing when using two common patterns in error constructors: wrapped sentinel errors and custom error types. TraceSkip works by accepting a number of stack frames to skip when defining the location of the traced errors it returns. In this example, the location is being set to the location of the callers of the error constructors, and not the constructors themselves.

package main

import (
	"errors"
	"fmt"

	"github.com/alnvdl/terr"
)

var ErrConnection = errors.New("connection error")

func connectionError(text string) error {
	return terr.TraceSkip(fmt.Errorf("%w: %s", ErrConnection, text), 1)
}

type ValidationError struct{ field, msg string }

func (e *ValidationError) Error() string {
	return e.msg
}

func NewValidationError(field, msg string) error {
	return terr.TraceSkip(&ValidationError{field, msg}, 1)
}

func main() {
	// It is considered a good practice in Go to never return sentinel errors
	// directly, but rather to wrap them like we do with connectionError here,
	// so they can be turned into custom errors later if needed, without
	// breaking callers in the process.
	// err1 will be annotated with the line number of the following line.
	err1 := connectionError("timeout")
	fmt.Printf("%@\n", err1)

	// errors.Is works.
	fmt.Println("\tIs ErrConnection:", errors.Is(err1, ErrConnection))
	fmt.Println("---")

	// If an error needs to include more details, a custom error type needs to
	// be used.
	// err2 will be annotated with the line number of the following line.
	err2 := NewValidationError("x", "x must be >= 0")
	fmt.Printf("%@\n", err2)

	// errors.As works.
	var customErr *ValidationError
	ok := errors.As(err2, &customErr)
	fmt.Println("\tIs ValidationError:", ok)
	fmt.Println("\tCustom error field:", customErr.field)
	fmt.Println("\tCustom error message:", customErr.msg)
}
Output:

Types

type ErrorTracer added in v1.0.12

type ErrorTracer interface {
	// error is the underlying error.
	error
	// Location identifies the file and line where error was created, traced,
	// wrapped or masked.
	Location() (string, int)
	// Children returns other traced errors that were traced, wrapped or
	// masked by this traced error.
	Children() []ErrorTracer
}

ErrorTracer is an object capable of tracing an error's location and possibly other related errors, forming an error tracing tree. Please note that implementing ErrorTracer is not enough to make an error a traced error: only errors returned by functions in this package are considered traced errors.

func TraceTree

func TraceTree(err error) ErrorTracer

TraceTree returns the root of the n-ary error tracing tree for err. Returns nil if err is not a traced error. This function can be used to represent the error tracing tree using custom formats.

Example

This example shows how to use the n-ary error tracing tree returned by TraceTree.

package main

import (
	"errors"
	"fmt"

	"github.com/alnvdl/terr"
)

func main() {
	nonTracedErr := errors.New("non-traced")
	tracedErr1 := terr.Newf("traced 1")
	tracedErr2 := terr.Newf("traced 2")
	newErr := terr.Newf("%w, %v, %w",
		nonTracedErr,
		tracedErr1,
		tracedErr2)

	printNode := func(node terr.ErrorTracer) {
		fmt.Printf("Error: %v\n", node.Error())
		file, line := node.Location()
		fmt.Printf("Location: %s:%d\n", file, line)
		fmt.Printf("Children: %v\n", node.Children())
		fmt.Println("---")
	}

	node := terr.TraceTree(newErr)
	printNode(node)
	printNode(node.Children()[0])
	printNode(node.Children()[1])
}
Output:

Jump to

Keyboard shortcuts

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