errors

errors + stack traces.
Key features of k1LoW/errors are:
- Retain the stack traces once stacked as far as possible.
- It is possible to output stack traces in structured data.
- It is possible to separate joined errors.
- Zero dependency
Usage
k1LoW/errors is a drop-in replacement for the standard errors package, so it is enough to swap the import path.
import (
// "errors"
"github.com/k1LoW/errors"
)
Then call errors.WithStack where an error is created or returned.
var ErrNotFound = errors.New("not found")
func query() error { return errors.WithStack(ErrNotFound) }
func fetch() error { return fmt.Errorf("fetch user: %w", query()) }
func handle() error { return errors.WithStack(fetch()) }
errors.StackTraces returns the stack traces held by the error.
err := handle()
fmt.Println(err)
// fetch user: not found
fmt.Println(errors.StackTraces(err))
// not found
// main.query
// /path/to/main.go:12
// main.fetch
// /path/to/main.go:13
// main.handle
// /path/to/main.go:14
// main.main
// /path/to/main.go:17
Note that the stack trace starts at main.query, which is where errors.WithStack was called first. The later errors.WithStack in main.handle does not overwrite it, so errors.WithStack can be called anywhere along the way without losing the original call site.
Attach a stack trace with defer
Because errors.WithStack keeps the first stack trace, it can be attached in a defer block covering the whole function.
func run() (err error) {
defer func() {
err = errors.WithStack(err)
}()
// ...
}
errors.WithStack(nil) returns nil, so the defer above is safe even when the function succeeds.
Output stack traces as structured data
The value returned by errors.StackTraces implements json.Marshaler, so it can be passed to log/slog or encoding/json as is.
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
logger.Error("request failed", slog.Any("stacktraces", errors.StackTraces(err)))
[
{
"error": "not found",
"frames": [
{ "name": "main.query", "file": "/path/to/main.go", "line": 12 },
{ "name": "main.fetch", "file": "/path/to/main.go", "line": 13 }
]
}
]
Separate joined errors
An error joined by errors.Join keeps the stack trace of each error, and errors.Errors splits it back into the individual errors.
func a() error { return errors.WithStack(ErrA) }
func b() error { return errors.WithStack(ErrB) }
func run() error { return errors.Join(a(), b()) }
err := run()
for _, e := range errors.Errors(err) {
fmt.Println(e)
}
// error a
// error b
fmt.Println(len(errors.StackTraces(err)))
// 2
Nested joins are flattened, so both errors.Errors and errors.StackTraces return a flat slice.
Example
https://go.dev/play/p/8zQvFThxI4O
Difference between errors and k1LoW/errors
API
MaxStackDepth (default 50) is the maximum number of frames recorded by errors.WithStack.
References