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.
Top comments (0)