SkillAgentSearch skills...

Errtrace

An alternative to stack traces for your Go errors

Install / Use

/learn @bracesdev/Errtrace
About this skill

Quality Score

0/100

Supported Platforms

Universal

Tags

README

errtrace

What if every function added its location to returned errors?

errtrace logo

CI Go Reference codecov

Introduction

errtrace is an experimental package to trace an error's return path — the return trace — through a Go program.

Where a stack trace tracks the code path that led to an error, a return trace tracks the code path that the error took to get to the user. Often these are the same path, but in Go they can diverge, since errors are values that can be transported across goroutines (e.g. with channels). When that happens, a return trace can be more useful than a stack trace.

This library is inspired by Zig's error return traces.

Features

  • Lightweight
    errtrace brings no other runtime dependencies with it.
  • Simple
    The library API is simple, straightforward, and idiomatic.
  • Easy
    The errtrace CLI will automatically instrument your code.
  • Fast
    On popular 64-bit systems, errtrace is much faster than capturing a stack trace.

Comparison with stack traces

With stack traces, caller information for the goroutine is captured once when the error is created.

In constrast, errtrace records the caller information incrementally, following the return path the error takes to get to the user. This approach works even if the error isn't propagated directly through function returns, and across goroutines.

Both approaches look similar when the error flows through function calls within the same goroutine, but can differ significantly when errors are passed outside of functions and across goroutines (e.g., channels).

Here's a real-world example that shows the benefits of errtrace tracing the return path by comparing a custom dial error returned for a HTTP request, which the net/http library uses a background goroutine for.

<details open> <summary>errtrace compared to a stack trace</summary> <table> <thead> <tr><td>errtrace</td><td>stack trace</td></tr> </thead> <tbody> <tr><td>
Error: connect rate limited

braces.dev/errtrace_test.rateLimitDialer
	/path/to/errtrace/example_http_test.go:72
braces.dev/errtrace_test.(*PackageStore).updateIndex
	/path/to/errtrace/example_http_test.go:59
braces.dev/errtrace_test.(*PackageStore).Get
	/path/to/errtrace/example_http_test.go:49
</td><td>
Error: connect rate limited
braces.dev/errtrace_test.rateLimitDialer
	/errtrace/example_stack_test.go:81
net/http.(*Transport).dial
	/goroot/src/net/http/transport.go:1190
net/http.(*Transport).dialConn
	/goroot/src/net/http/transport.go:1625
net/http.(*Transport).dialConnFor
	/goroot/src/net/http/transport.go:1467
runtime.goexit
	/goroot/src/runtime/asm_arm64.s:1197
</td></tr> <tr> <td>errtrace reports the method that triggered the HTTP request</td> <td>stack trace shows details of how the HTTP client creates a connection</td> </tr> </tbody> </table> </details>

errtrace also reduces the performance impact of capturing caller information for errors that are handled rather than returned to the user, as the information is captured incrementally. Stack traces pay a fixed cost to capture caller information even if the error is handled immediately by the caller close to where the error is created.

Try it out

Try out errtrace with your own code:

  1. Install the CLI.

    go install braces.dev/errtrace/cmd/errtrace@latest
    
  2. Switch to your Git repository and instrument your code.

    errtrace -w ./...
    
  3. Let go mod tidy install the errtrace Go module for you.

    go mod tidy
    
  4. Run your tests to ensure everything still works. You may see failures if you're comparing errors with == on critical paths or if you're type-casting errors directly. See Error wrapping for more details.

    go test ./...
    
  5. Print return traces for errors in your code. To do this, you can use the errtrace.FormatString function or format the error with %+v in fmt.Printf-style functions.

    if err != nil {
      fmt.Fprintf(os.Stderr, "%+v", err)
    }
    

Return traces printed by errtrace will include the error message and the path the error took until it was printed. The output will look roughly like this:

error message

example.com/myproject.MyFunc
	/home/user/myproject/myfile.go:123
example.com/myproject.CallerOfMyFunc
	/home/user/myproject/another_file.go:456
[...]

Here's a real-world example of errtrace in action:

<details> <summary>Example</summary>
doc2go: parse file: /path/to/project/example/foo.go:3:1: expected declaration, found invalid

go.abhg.dev/doc2go/internal/gosrc.parseFiles
        /path/to/project/internal/gosrc/parser.go:85
go.abhg.dev/doc2go/internal/gosrc.(*Parser).ParsePackage
        /path/to/project/internal/gosrc/parser.go:44
main.(*Generator).renderPackage
        /path/to/project/generate.go:193
main.(*Generator).renderTree
        /path/to/project/generate.go:141
main.(*Generator).renderTrees
        /path/to/project/generate.go:118
main.(*Generator).renderPackageIndex
        /path/to/project/generate.go:149
main.(*Generator).renderTree
        /path/to/project/generate.go:137
main.(*Generator).renderTrees
        /path/to/project/generate.go:118
main.(*Generator).renderPackageIndex
        /path/to/project/generate.go:149
main.(*Generator).renderTree
        /path/to/project/generate.go:137
main.(*Generator).renderTrees
        /path/to/project/generate.go:118
main.(*Generator).Generate
        /path/to/project/generate.go:110
main.(*mainCmd).run
        /path/to/project/main.go:199

Note the some functions repeat in this trace because the functions are mutually recursive.

</details>

Why is this useful?

In Go, errors are values. This means that an error can be passed around like any other value. You can store it in a struct, pass it through a channel, etc. This level of flexibility is great, but it can also make it difficult to track down the source of an error. A stack trace stored in an error — recorded at the error site — becomes less useful as the error moves through the program. When it's eventually surfaced to the user, we've lost a lot of context about its origin.

With errtrace, we instead record the path the program took from the error site to get to the user — the return trace. Not only can this be more useful than a stack trace, it tends to be much faster and more lightweight as well.

Installation

Install errtrace with Go modules:

go get braces.dev/errtrace@latest

If you want to use the CLI, use go install.

go install braces.dev/errtrace/cmd/errtrace@latest

Usage

errtrace offers the following modes of usage:

Manual instrumentation

import "braces.dev/errtrace"

Under manual instrumentation, you're expected to import errtrace, and wrap errors at all return sites like so:

// ...
if err != nil {
    return errtrace.Wrap(err)
}
<details> <summary>Example</summary>

Given a function like the following:

func writeToFile(path string, src io.Reader) error {
  dst, err := os.Create(path)
  if err != nil {
    return err
  }
  defer dst.Close()

  if _, err := io.Copy(dst, src); err != nil {
    return err
  }

  return nil
}

With errtrace, you'd change it to:

func writeToFile(path string, src io.Reader) error {
  dst, err := os.Create(path)
  if err != nil {
    return errtrace.Wrap(err)
  }
  defer dst.Close()

  if _, err := io.Copy(dst, src); err != nil {
    return errtrace.Wrap(err)
  }

  return nil
}

It's important that the errtrace.Wrap function is called inside the same function that's actually returning the error. A helper function will not suffice.

</details>

Automatic instrumentation

If manual instrumentation is too much work (we agree), we've included a tool that will automatically instrument all your code with errtrace.

First, install the tool. Then, run it on your code:

errtrace -w path/to/file.go path/to/another/file.go

Instead of specifying individual files, you can also specify a Go package pattern. For example:

errtrace -w example.com/path/to/package
errtrace -w ./...

Automatic instrumentation on save

errtrace can be set be setup as a custom formatter in your editor, similar to gofmt or goimports.

Automatic compile-time instrumentation

experimental

errtrace can rewrite files as part of compilation by passing a -toolexec flag. Build your package or binary with go build -toolexec=errtrace and all packages that

View on GitHub
GitHub Stars791
CategoryDevelopment
Updated3d ago
Forks11

Languages

Go

Security Score

95/100

Audited on Mar 24, 2026

No findings