Documentation
¶
Overview ¶
Package errtrace provides the ability to track a return trace for errors. This differs from a stack trace in that it is not a snapshot of the call stack at the time of the error, but rather a trace of the path taken by the error as it was returned until it was finally handled.
Wrapping errors ¶
Use the Wrap function at a return site to annotate it with the position of the return.
// Before
if err != nil {
return err
}
// After
if err != nil {
return errtrace.Wrap(err)
}
Formatting return traces ¶
errtrace provides the Format and FormatString functions to obtain the return trace of an error.
errtrace.Format(os.Stderr, err)
See Format for details of the output format.
Additionally, errors returned by errtrace will also print a trace if formatted with the %+v verb when used with a Printf-style function.
log.Printf("error: %+v", err)
Unwrapping errors ¶
Use the UnwrapFrame function to unwrap a single frame from an error.
for err != nil {
frame, inner, ok := errtrace.UnwrapFrame(err)
if !ok {
break // end of trace
}
printFrame(frame)
err = inner
}
See the UnwrapFrame example test for a more complete example.
See also ¶
https://github.com/bracesdev/errtrace.
Example (GetCaller) ¶
package main
import (
"errors"
"fmt"
"braces.dev/errtrace"
"braces.dev/errtrace/internal/tracetest"
)
func f1Wrap() error {
return wrap(f2Wrap(), "u=1")
}
func f2Wrap() error {
return wrap(f3Wrap(), "method=order")
}
func f3Wrap() error {
return wrap(errors.New("failed"), "foo")
}
func wrap(err error, fields ...string) error {
return errtrace.GetCaller().
Wrap(fmt.Errorf("%w %v", err, fields))
}
func main() {
got := errtrace.FormatString(f1Wrap())
// make trace agnostic to environment-specific location
// and less sensitive to line number changes.
fmt.Println(tracetest.MustClean(got))
}
Output: failed [foo] [method=order] [u=1] braces.dev/errtrace_test.f3Wrap /path/to/errtrace/example_errhelper_test.go:3 braces.dev/errtrace_test.f2Wrap /path/to/errtrace/example_errhelper_test.go:2 braces.dev/errtrace_test.f1Wrap /path/to/errtrace/example_errhelper_test.go:1
Example (Http) ¶
package main
import (
"fmt"
"io"
"net"
"net/http"
"strings"
"braces.dev/errtrace"
"braces.dev/errtrace/internal/tracetest"
)
func main() {
tp := &http.Transport{Dial: rateLimitDialer}
client := &http.Client{Transport: tp}
ps := &PackageStore{
client: client,
}
_, err := ps.Get()
fmt.Printf("Error fetching packages: %+v\n", tracetest.MustClean(errtrace.FormatString(err)))
}
type PackageStore struct {
client *http.Client
packagesCached []string
}
func (ps *PackageStore) Get() ([]string, error) {
if ps.packagesCached != nil {
return ps.packagesCached, nil
}
packages, err := ps.updateIndex()
if err != nil {
return nil, errtrace.Wrap(err)
}
ps.packagesCached = packages
return packages, nil
}
func (ps *PackageStore) updateIndex() ([]string, error) {
resp, err := ps.client.Get("http://example.com/packages.index")
if err != nil {
return nil, errtrace.Wrap(err)
}
contents, err := io.ReadAll(resp.Body)
if err != nil {
return nil, errtrace.Wrap(err)
}
return strings.Split(string(contents), ","), nil
}
func rateLimitDialer(network, addr string) (net.Conn, error) {
// for testing, always return an error.
return nil, errtrace.New("connect rate limited")
}
Output: Error fetching packages: Get "http://example.com/packages.index": connect rate limited braces.dev/errtrace_test.rateLimitDialer /path/to/errtrace/example_http_test.go:3 braces.dev/errtrace_test.(*PackageStore).updateIndex /path/to/errtrace/example_http_test.go:2 braces.dev/errtrace_test.(*PackageStore).Get /path/to/errtrace/example_http_test.go:1
Example (LogWithSlog) ¶
package main
import (
"fmt"
"log/slog"
"strings"
"braces.dev/errtrace"
"braces.dev/errtrace/internal/tracetest"
)
func f1() error {
return errtrace.Wrap(f2())
}
func f2() error {
return errtrace.Wrap(f3())
}
func f3() error {
return errtrace.New("failed")
}
func main() {
// This example demonstrates how to log an errtrace-wrapped error
// with the slog package.
// Unlike LogAttr, we're able to use any key name here.
logger, printLogOutput := newExampleLogger()
if err := f1(); err != nil {
logger.Error("f1 failed", "my-error", err)
}
printLogOutput()
}
// newExampleLogger creates a new slog.Logger for use in examples.
// It omits timestamps from the output to allow for output matching,
// and cleans paths in trace output to make them environment-agnostic.
func newExampleLogger() (logger *slog.Logger, printOutput func()) {
var buf strings.Builder
return slog.New(slog.NewJSONHandler(&buf, &slog.HandlerOptions{
ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
if len(groups) == 0 && a.Key == slog.TimeKey {
return slog.Attr{}
}
return a
},
})), func() {
fmt.Println(tracetest.MustClean(buf.String()))
buf.Reset()
}
}
Output: {"level":"ERROR","msg":"f1 failed","my-error":"failed\n\nbraces.dev/errtrace_test.f3\n\t/path/to/errtrace/example_trace_test.go:3\nbraces.dev/errtrace_test.f2\n\t/path/to/errtrace/example_trace_test.go:2\nbraces.dev/errtrace_test.f1\n\t/path/to/errtrace/example_trace_test.go:1\n"}
Example (Trace) ¶
package main
import (
"fmt"
"braces.dev/errtrace"
"braces.dev/errtrace/internal/tracetest"
)
func f1() error {
return errtrace.Wrap(f2())
}
func f2() error {
return errtrace.Wrap(f3())
}
func f3() error {
return errtrace.New("failed")
}
func main() {
got := errtrace.FormatString(f1())
// make trace agnostic to environment-specific location
// and less sensitive to line number changes.
fmt.Println(tracetest.MustClean(got))
}
Output: failed braces.dev/errtrace_test.f3 /path/to/errtrace/example_trace_test.go:3 braces.dev/errtrace_test.f2 /path/to/errtrace/example_trace_test.go:2 braces.dev/errtrace_test.f1 /path/to/errtrace/example_trace_test.go:1
Example (Tree) ¶
package main
import (
"errors"
"fmt"
"strings"
"braces.dev/errtrace"
"braces.dev/errtrace/internal/tracetest"
)
func normalErr(i int) error {
return fmt.Errorf("std err %v", i)
}
func wrapNormalErr(i int) error {
return errtrace.Wrap(normalErr(i))
}
func nestedErrorList(i int) error {
return errors.Join(
normalErr(i),
wrapNormalErr(i+1),
)
}
func main() {
errs := errtrace.Wrap(errors.Join(
normalErr(1),
wrapNormalErr(2),
nestedErrorList(3),
))
got := errtrace.FormatString(errs)
// make trace agnostic to environment-specific location
// and less sensitive to line number changes.
fmt.Println(trimTrailingSpaces(tracetest.MustClean(got)))
}
func trimTrailingSpaces(s string) string {
lines := strings.Split(s, "\n")
for i := range lines {
lines[i] = strings.TrimRight(lines[i], " \t")
}
return strings.Join(lines, "\n")
}
Output: +- std err 1 | +- std err 2 | | braces.dev/errtrace_test.wrapNormalErr | /path/to/errtrace/example_tree_test.go:1 | | +- std err 3 | | | +- std err 4 | | | | braces.dev/errtrace_test.wrapNormalErr | | /path/to/errtrace/example_tree_test.go:1 | | +- std err 3 | std err 4 | std err 1 std err 2 std err 3 std err 4 braces.dev/errtrace_test.Example_tree /path/to/errtrace/example_tree_test.go:2
Index ¶
- func Errorf(format string, args ...any) error
- func Format(w io.Writer, target error) (err error)
- func FormatString(target error) string
- func New(text string) error
- func UnwrapFrame(err error) (frame runtime.Frame, inner error, ok bool)
- func Wrap(err error) error
- func Wrap2[T any](t T, err error) (T, error)
- func Wrap3[T1, T2 any](t1 T1, t2 T2, err error) (T1, T2, error)
- func Wrap4[T1, T2, T3 any](t1 T1, t2 T2, t3 T3, err error) (T1, T2, T3, error)
- func Wrap5[T1, T2, T3, T4 any](t1 T1, t2 T2, t3 T3, t4 T4, err error) (T1, T2, T3, T4, error)
- func Wrap6[T1, T2, T3, T4, T5 any](t1 T1, t2 T2, t3 T3, t4 T4, t5 T5, err error) (T1, T2, T3, T4, T5, error)
- type Caller
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func Errorf ¶
Errorf creates an error message according to a format specifier and returns the string as a value that satisfies error.
It's equivalent to fmt.Errorf followed by Wrap to add caller information.
func Format ¶
Format writes the return trace for given error to the writer. The output takes a format similar to the following:
<error message> <function> <file>:<line> <caller of function> <file>:<line> [...]
Any error that has a method `TracePC() uintptr` will contribute to the trace. If the error doesn't have a return trace attached to it, only the error message is reported. If the error is comprised of multiple errors (e.g. with errors.Join), the return trace of each error is reported as a tree.
Returns an error if the writer fails.
func FormatString ¶
FormatString writes the return trace for err to a string. Any error that has a method `TracePC() uintptr` will contribute to the trace. See Format for details of the output format.
func New ¶
New returns an error with the supplied text.
It's equivalent to errors.New followed by Wrap to add caller information.
func UnwrapFrame ¶ added in v0.4.0
UnwrapFrame unwraps the outermost frame from the given error, returning it and the inner error. ok is true if the frame was successfully extracted, and false otherwise, or if the error is not an errtrace error.
You can use this for structured access to trace information.
Any error that has a method `TracePC() uintptr` will contribute a frame to the trace.
Example ¶
package main
import (
"errors"
"fmt"
"runtime"
"strings"
"braces.dev/errtrace"
"braces.dev/errtrace/internal/tracetest"
)
func f1() error {
return errtrace.Wrap(f2())
}
func f2() error {
return errtrace.Wrap(f3())
}
func f3() error {
return errtrace.New("failed")
}
func f4() error {
return errtrace.Wrap(fmt.Errorf("wrapped: %w", f1()))
}
func main() {
var frames []runtime.Frame
current := f4()
for current != nil {
frame, inner, ok := errtrace.UnwrapFrame(current)
if !ok {
// If the error is not wrapped with errtrace,
// unwrap it directly with errors.Unwrap.
current = errors.Unwrap(current)
continue
// Note that this example does not handle multi-errors,
// for example those returned by errors.Join.
// To handle those, this loop would need to also check
// for the 'Unwrap() []error' method on the error.
}
frames = append(frames, frame)
current = inner
}
var trace strings.Builder
for _, frame := range frames {
fmt.Fprintf(&trace, "%s\n\t%s:%d\n", frame.Function, frame.File, frame.Line)
}
fmt.Println(tracetest.MustClean(trace.String()))
}
Output: braces.dev/errtrace_test.f4 /path/to/errtrace/example_trace_test.go:4 braces.dev/errtrace_test.f1 /path/to/errtrace/example_trace_test.go:1 braces.dev/errtrace_test.f2 /path/to/errtrace/example_trace_test.go:2 braces.dev/errtrace_test.f3 /path/to/errtrace/example_trace_test.go:3
func Wrap ¶
Wrap adds information about the program counter of the caller to the error. This is intended to be used at all return points in a function. If err is nil, Wrap returns nil.
func Wrap2 ¶
Wrap2 is used to Wrap the last error return when returning 2 values. This is useful when returning multiple returns from a function call directly:
return Wrap2(fn())
Wrap2 is used by the CLI to avoid line number changes.
func Wrap3 ¶
Wrap3 is used to Wrap the last error return when returning 3 values. This is useful when returning multiple returns from a function call directly:
return Wrap3(fn())
Wrap3 is used by the CLI to avoid line number changes.
func Wrap4 ¶
Wrap4 is used to Wrap the last error return when returning 4 values. This is useful when returning multiple returns from a function call directly:
return Wrap4(fn())
Wrap4 is used by the CLI to avoid line number changes.
func Wrap5 ¶
Wrap5 is used to Wrap the last error return when returning 5 values. This is useful when returning multiple returns from a function call directly:
return Wrap5(fn())
Wrap5 is used by the CLI to avoid line number changes.
func Wrap6 ¶
func Wrap6[T1, T2, T3, T4, T5 any](t1 T1, t2 T2, t3 T3, t4 T4, t5 T5, err error) (T1, T2, T3, T4, T5, error)
Wrap6 is used to Wrap the last error return when returning 6 values. This is useful when returning multiple returns from a function call directly:
return Wrap6(fn())
Wrap6 is used by the CLI to avoid line number changes.
Types ¶
type Caller ¶ added in v0.4.0
type Caller struct {
// contains filtered or unexported fields
}
Caller represents a single caller frame, and is intended for error helpers to capture caller information for wrapping. See GetCaller for details.
func GetCaller ¶ added in v0.4.0
func GetCaller() Caller
GetCaller captures the program counter of a caller, primarily intended for error helpers so caller information captures the helper's caller.
Callers of this function should be marked '//go:noinline' to avoid inlining, as GetCaller expects to skip the caller's stack frame.
//go:noinline
func Wrapf(err error, msg string, args ...any) {
caller := errtrace.GetCaller()
err := ...
return caller.Wrap(err)
}
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
cmd
|
|
|
errtrace
command
errtrace instruments Go code with error return tracing.
|
errtrace instruments Go code with error return tracing. |
|
internal
|
|
|
diff
Package diff provides utilities for comparing strings and slices to produce a readable diff output for tests.
|
Package diff provides utilities for comparing strings and slices to produce a readable diff output for tests. |
|
pc
Package pc provides access to the program counter to determine the caller of a function.
|
Package pc provides access to the program counter to determine the caller of a function. |
|
tracetest
Package tracetest provides utilities for errtrace to test error trace output conveniently.
|
Package tracetest provides utilities for errtrace to test error trace output conveniently. |
