DEV Community

JackTT
JackTT

Posted on

Tracing error strack in Golang

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 } 
Enter fullscreen mode Exit fullscreen mode

When an error occurs, you’ll get:

json: cannot unmarshal string into Go value of type int 
Enter fullscreen mode Exit fullscreen mode

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) 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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 } } } 
Enter fullscreen mode Exit fullscreen mode

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) } } 
Enter fullscreen mode Exit fullscreen mode

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)