> ## Content Index
> Fetch the complete content index at: https://debugly.dev/llms.txt
> Use this file to discover other available public pages before exploring further.

# Reading a Go Panic Stack Trace Without Guessing
- URL: https://debugly.dev/go-nil-pointer-dereference-goroutine-stack/
- Published: 2026-09-27T11:30:00.000Z
- Updated: 2026-10-17T05:57:27.000Z
- Description: The top frame of a nil pointer panic is rarely the cause. The goroutine dump tells you which goroutine, which value was nil and which send made it so, if…
- Author: Rohit Bhadani
- Tags: Error Autopsy, Debugging, Tooling

The panic said nil pointer dereference and printed a stack, and the engineer read the stack from the top, found a line in a helper, and spent a day hardening the helper against nil. The helper was fine. The nil value was created three goroutines and forty lines away, and the top frame was merely the place the bad value finally arrived, which is where panics always print, and the least informative place to look.

I have hit this enough times that I now check for it before I check for anything clever.

A Go panic stack is not a narrative. It is a snapshot of one goroutine at the moment of failure, plus, in a full dump, every other goroutine frozen mid work. Reading it well is a technique, and the technique is to read it as a crime scene where the body is not the murder.

This was Go 1.23, and the panic was a typed nil interface, which is the subtlest member of the family and worth its own section.

## The quick version

The line that panicked tells you where the nil was dereferenced, not where it became nil. The value was already nil when it entered that frame. So the investigation walks down the stack, frame by frame, asking at each one: did this frame create the value, or receive it. The first frame that created it, or that could have checked it, is where the defect lives, and everything above is just the delivery route.

The dump gives you the address of the question. The discipline is to refuse the top frame's answer.

## Reading the dump in order

The full goroutine dump, from a crash or from a deliberate signal, is the primary source. Read it in this order.

First, the panicking goroutine's stack, to name the dereferenced expression. The panic message names the memory address, usually zero, and the failing instruction's source line. Note which receiver or pointer was nil. That is the variable to trace.

Second, walk down that goroutine's frames to the first one that constructed the variable. Constructors in Go are returns, map lookups, type assertions and channel receives, and each has a nil failure mode. A map lookup returns the zero value when absent. A type assertion without the comma ok form panics or yields nil. A channel receive from a closed channel returns zero. The frame that did one of these without checking is the suspect.

Third, read the other goroutines, because in concurrent code the nil often came through a channel or a shared map, and the goroutine that closed the channel or deleted the map entry is in the dump, frozen at the send. The dump is the only place you see both sides of the handoff, which is why single stack trace reading fails on concurrent nils.

## The typed nil trap

The subtlest variant is the interface holding a nil concrete pointer, which is not a nil interface. A function returning an error as an interface, returning a typed nil pointer of a concrete error type, produces a value that compares not equal to nil, passes every `if err != nil` check, and then dereferences to a panic at the method call. The check that was supposed to guard is precisely what let it through, because the interface is non nil.

This is the most common source of "we checked for nil and still panicked", and the fix is at the producer: return a truly nil interface, which means returning the literal nil, not a nil pointer of a concrete type. The review question is every return of a pointer typed as an interface, and whether the nil case returns untyped nil.

## The map and channel zeros

The second family is the zero value masquerading as data. A map access for a missing key returns the zero value of the value type, which for a pointer or struct with pointer fields is a nil that looks like a fetched record. The comma ok idiom is the check, and its absence is the defect.

A receive from a closed channel returns the zero value with ok false, and a loop that ranges over a channel exits cleanly, but a loop that receives without checking closed treats the zero as a message. Both produce a nil that arrived through a perfectly ordinary receive, and the dump shows the receive, not the close.

## Making the dump arrive with context

A panic in production is only as good as the dump it leaves, and Go gives you the tools to guarantee it. Set the crash on panic behaviour you want, capture the full dump on the signal, and, for the panics you anticipate, recover at the process boundary and log the recovered value with the stack, because a recovered panic with a logged dump is an incident you can read, while an unrecovered one in a container that restarts is a rumour.

The recover at the boundary must log, not swallow. Swallowing the panic turns the crime scene into a missing person report, which is the observability version of the error handling defect in [reviewing error handling paths](https://debugly.dev/reviewing-error-handling-paths/).

## The takeaway

A nil pointer panic names the dereference, and the dump names the handoff. Walk down the panicking goroutine to the constructor, read the other goroutines for the sender, and treat a passing nil check as the signature of the typed nil, not as evidence of safety.

The top frame is where the value died. The constructor is where it was born broken, and the dump, read in order, is the only witness that saw both. The concurrency cousin, where the shared value changes between check and use, is [reviewing shared state under contention](https://debugly.dev/reviewing-locks-and-shared-state/), which is why the two fixes look alike.

---

Published on [debugly.dev](https://debugly.dev/) by [Rohit Bhadani](https://debugly.dev/about/) | Founder of [Krova Cloud](https://krova.cloud/?ref=debugly.dev)