Problem: No Stack Trace in Native Errors
Consider this Go snippet:
func function3() error {
var result1 map[string]int
input1 := `{"key": "value"}`
if err := json.Unmarshal([]byte(input1), &result1); err != nil {
return err
}
var result2 map[string]int
input2 := `{"key": 123}`
if err := json.Unmarshal([]byte(input2), &result2); err != nil {
return err
}
return nil
}
When an error occurs, you’ll get:
json: cannot unmarshal string into Go value of type int
But which line caused it? The first or second Unmarshal?
Without a stack trace, it’s unclear.
Solution: Attach a Stack Trace
Using github.com/cockroachdb/errors, you can wrap errors with errors.WithStack
:
return errors.WithStack(err)
Example output with fmt.Printf("%+v", err)
:
json: cannot unmarshal string into Go value of type int
(1) attached stack trace
-- stack trace:
| main.function3
| /golang-test/errror_tracing/main.go:28
| main.function2
| /backend/golang-test/errror_tracing/main.go:19
| main.function1
| /backend/golang-test/errror_tracing/main.go:15
| main.main
| /backend/golang-test/errror_tracing/main.go:10
| runtime.main
| /Users/jack/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.23.9.darwin-arm64/src/runtime/proc.go:272
| runtime.goexit
| /Users/jack/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.23.9.darwin-arm64/src/runtime/asm_arm64.s:1223
Now you know exactly where the error occurred.
Under the Hood: How Stack Capture Works
Go’s runtime.Callers()
lets you capture the current stack:
func printStack() {
// Declare an array to hold up to 10 program counters (PCs).
// PCs represent the addresses of the function calls in the call stack.
var pcs [10]uintptr
// Capture the call stack PCs, skipping the first 2 frames:
// 0 = runtime.Callers, 1 = printStack itself.
// pcs[:] converts the array to a slice.
// n is the number of PCs actually captured.
n := runtime.Callers(2, pcs[:])
// Create a Frames iterator from the captured PCs slice,
// which translates PCs into human-readable function/file/line info.
frames := runtime.CallersFrames(pcs[:n])
// Loop through the frames iterator until there are no more frames.
for {
// Get the next frame and a boolean indicating if more frames remain.
frame, more := frames.Next()
// Print the function name, source file, and line number of this frame.
fmt.Printf("%s\n\t%s:%d\n", frame.Function, frame.File, frame.Line)
// If there are no more frames, break out of the loop.
if !more {
break
}
}
}
This function prints the call path to its current point in the code.
Rebuilding errors.WithStack
Here’s a minimal reimplementation of errors.WithStack
:
type errWithStack struct {
err error
stack []uintptr
}
func WithStack(err error) error {
if err == nil {
return nil
}
var pcs [32]uintptr
n := runtime.Callers(3, pcs[:])
return &errWithStack{err, pcs[:n]}
}
func (e *errWithStack) Error() string { return e.err.Error() }
// Format implemnets fmt.Formatter
func (e *errWithStack) Format(s fmt.State, verb rune) {
switch verb {
case 'v':
if s.Flag('+') {
fmt.Fprint(s, e.err)
frames := runtime.CallersFrames(e.stack)
for {
f, more := frames.Next()
fmt.Fprintf(s, "\n%s\n\t%s:%d", f.Function, f.File, f.Line)
if !more {
break
}
}
return
}
fallthrough
case 's':
fmt.Fprint(s, e.err)
}
}
Behavior:
-
fmt.Println(err)
→ shows the error message only -
fmt.Printf("%+v", err)
→ includes full stack trace
Summary
- Standard errors in Go don’t include stack traces.
- Using libraries like
cockroachdb/errors
gives you precise visibility into where errors happen — critical for debugging complex applications.