Tracing error strack in Golang
JackTT

JackTT @jacktt

About: Software Engineer | DevOps | Gopher 🇻🇳

Location:
localhost
Joined:
Jun 21, 2023

Tracing error strack in Golang

Publish Date: May 23
0 0

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.

Comments 0 total

    Add comment