Quick Fix for Debugging Panic Failures in Go

So I was working on LiveAPI, a tool I built to generate interactive API docs by consuming a Git repository. Every generation gets tagged with a job_id, and everything goes well—until it doesn't. Sometimes, the server would crash. Not your classic panic("oops"). I'm talking about real crashes: slice index out of range, nil pointer dereference—stuff that doesn’t announce itself politely. What I Needed Something like: Catch panics Log the job ID that was last printed to stdout Capture the stack trace Save everything to a file for post-mortem ⚡️ The Hack I ended up redirecting os.Stdout into a buffer, searching for the last printed job ID using a regex, and then dumping all of that to a log file. Here’s how I did it: ✨ errorcodes/panic_logger.go var ( outputBuffer bytes.Buffer bufferLock sync.Mutex ) func init() { pipeReader, pipeWriter, err := os.Pipe() if err != nil { log.Fatal().Err(err).Msg("Failed to create pipe") } os.Stdout = pipeWriter go func() { scanner := bufio.NewScanner(pipeReader) for scanner.Scan() { line := scanner.Text() bufferLock.Lock() outputBuffer.WriteString(line + "\n") bufferLock.Unlock() fmt.Fprintln(os.Stderr, line) // still print to terminal } }() } This intercepts every fmt.Println() you do.

Apr 7, 2025 - 19:14
 0
Quick Fix for Debugging Panic Failures in Go

So I was working on LiveAPI, a tool I built to generate interactive API docs by consuming a Git repository.

Every generation gets tagged with a job_id, and everything goes well—until it doesn't.

Sometimes, the server would crash.

Not your classic panic("oops").

I'm talking about real crashes: slice index out of range, nil pointer dereference—stuff that doesn’t announce itself politely.

What I Needed

Something like:

  • Catch panics
  • Log the job ID that was last printed to stdout
  • Capture the stack trace
  • Save everything to a file for post-mortem

⚡️ The Hack

I ended up redirecting os.Stdout into a buffer, searching for the last printed job ID using a regex, and then dumping all of that to a log file.

Here’s how I did it:

errorcodes/panic_logger.go

var (
    outputBuffer bytes.Buffer
    bufferLock   sync.Mutex
)

func init() {
    pipeReader, pipeWriter, err := os.Pipe()
    if err != nil {
        log.Fatal().Err(err).Msg("Failed to create pipe")
    }

    os.Stdout = pipeWriter

    go func() {
        scanner := bufio.NewScanner(pipeReader)
        for scanner.Scan() {
            line := scanner.Text()

            bufferLock.Lock()
            outputBuffer.WriteString(line + "\n")
            bufferLock.Unlock()

            fmt.Fprintln(os.Stderr, line) // still print to terminal
        }
    }()
}

This intercepts every fmt.Println() you do.