Which Go runtime failures print `fatal error:` instead of raising a recoverable panic?
answer
- read the first word of the output
- severity is not the dividing line
- broken invariants versus one bad operation
- nil map panics, shared map is fatal
- deadlock, out of memory, stack overflow
basics
~20 sConcurrent map access, the all-goroutines-asleep deadlock, out of memory, stack overflow and unlocking an unlocked mutex are fatal. Nil dereferences, index out of range, failed type assertions, divide by zero and nil-map writes are ordinary panics.
solid answer
~50 sThe fatal group is small and worth memorising: `concurrent map writes` and `concurrent map read and map write`, `all goroutines are asleep - deadlock!`, `out of memory`, `stack overflow`, and the `sync` package's own aborts such as unlocking an unlocked mutex. Everything else the runtime raises is a panic and can be recovered: nil pointer dereference, index or slice bounds out of range, a failed type assertion, integer divide by zero, `assignment to entry in nil map`, sending on a closed channel, and any `panic` you write yourself. The dividing line is not severity, it is soundness. A panic means one goroutine attempted an invalid operation while the runtime and your data structures are still intact, so unwinding that goroutine is meaningful. A fatal error means an invariant of the runtime or of shared state is already broken, or the process as a whole cannot proceed, so there is nothing sensible for user code to do next.
code
go · 5 linesvar m map[string]int
m["a"] = 1 // panic: assignment to entry in nil map -- recoverable
var mu sync.Mutex
mu.Unlock() // fatal error: sync: unlock of unlocked mutex -- not recoverablego deeper
Learn to read the first line of a crash. panic: means code could have recovered from it; fatal error: means nothing could. Knowing that a nil-map write is the first kind is a good anchor.
Be able to name the fatal group and give the reason for the split: broken shared or runtime invariants versus a single invalid operation in one goroutine. The nil-map versus shared-map contrast is the cleanest way to show you understand it.
Turn the split into design guidance: what you refuse to guard with recover, what must be durable rather than deferred, and where you bound recursion depth because a stack overflow cannot be caught.
Decide the team's stance. Which crash classes are acceptable failure modes handled by restart, which must be engineered out before release, and whether race-enabled test runs gate a release for concurrency-heavy packages.
## The two buckets Go splits run-time failures into recoverable panics and unrecoverable fatal errors. You can tell them apart from the first line of output: a panic starts with `panic:` and a fatal error starts with `fatal error:`. ### Recoverable panics These unwind the goroutine's stack, run its deferred calls, and can be stopped by a `recover` in a directly deferred function: - nil pointer dereference (`invalid memory address or nil pointer dereference`) - index out of range, slice bounds out of range - a failed type assertion without the comma-ok form - integer divide by zero - `assignment to entry in nil map` - send on a closed channel, close of a closed or nil channel - any `panic(v)` your own code calls, including `log.Panicf` ### Unrecoverable fatal errors These skip unwinding entirely: the runtime freezes the program, prints the banner and a stack dump of every goroutine, and exits. - `concurrent map writes` and `concurrent map read and map write` — best-effort detection of unsynchronised access to the same map - `all goroutines are asleep - deadlock!` — the scheduler has nothing it could ever run again - `out of memory` — the runtime could not obtain memory from the operating system - `stack overflow` — a goroutine's stack grew past the maximum, which is 1 GB on 64-bit platforms by default and adjustable with `runtime/debug.SetMaxStack`; the practical cause is unbounded recursion - `sync: unlock of unlocked mutex` and its `sync.RWMutex` equivalents - `unexpected signal during runtime execution`, which usually means a fault inside C code reached through cgo ## The principle behind the split It is tempting to read the split as severity — big problems are fatal, small ones are panics — but that is wrong, and the counterexample is the pair of map failures. Writing to a **nil** map is a panic; two goroutines writing the **same** map is fatal. In the first case exactly one goroutine did something invalid and nothing else in the process is affected, so unwinding it and letting a handler decide is coherent. In the second case unsynchronised writers may have left the map's internal structure inconsistent, and every future reader of that map is now unsound. There is nothing safe to continue with. The same reading explains the rest of the list. A deadlock is a property of the entire program, and there is no runnable goroutine left to execute a handler. Out of memory means running more code may itself need memory that is unavailable. Stack overflow means there is no room to push the frame that a deferred call would need. Unlocking an unlocked mutex means the program's synchronisation reasoning is already wrong, and the `sync` package refuses to let you paper over it. ## Where an unrecovered panic fits An unrecovered panic ends the process too, and it also dumps every goroutine and exits with status 2, which is why the two are easy to confuse in a log. The difference is what happened on the way: the panic ran the deferred calls of the panicking goroutine as it unwound, and any of them could have stopped it. The fatal error ran nothing. If you see `panic:` at the top, some code chose not to handle it; if you see `fatal error:`, no code was ever offered the chance. ## Why interviewers ask this The list itself is trivia, but the reasoning behind it is not. A candidate who has internalised the split writes different code: they do not wrap concurrent map access in a `recover` and call it safe, they do not expect a `defer` to flush a log buffer on every possible exit, and they treat `fatal error:` in a log as a design bug to remove rather than an exception to handle. ## Practical consequences - Never rely on deferred cleanup for correctness of something that must survive any crash; make it durable at the point it matters. - Shared maps need synchronisation or single-goroutine ownership; the fatal error is a late, unreliable detector, and a `-race` build in CI is the systematic one. - Deep recursion over untrusted input is a crash risk, not a catchable error; bound the depth explicitly.
- Is a `panic` you call yourself ever turned into a fatal error?No. If nothing recovers it, the output still begins with `panic:`, not `fatal error:`, and the deferred calls of the panicking goroutine ran as the stack unwound. It ends the process and exits with status 2 like a fatal error, but the mechanism and the chance to intervene were completely different.
- What actually causes `fatal error: stack overflow` in Go?Unbounded recursion, almost always. Goroutine stacks start small and grow on demand, so the failure appears only when the stack passes the maximum — 1 GB on 64-bit by default, changeable with `runtime/debug.SetMaxStack`. Because it is fatal, you cannot catch it with `recover`; recursion over attacker-controlled structures needs an explicit depth bound.
- Is `fatal error: out of memory` the same thing as the operating system killing the process?No. That banner is the Go runtime failing to obtain memory and aborting itself, so you get Go output and a goroutine dump. If the kernel kills the process instead, there is no Go output at all — the process simply disappears. Which of the two you see is the first fork in a memory investigation.
saying these in an interview costs you the question
- Says fatal versus panic is about how serious the failure is
- Thinks all runtime map errors are recoverable
- Claims recover can catch a stack overflow
- Assumes every crash line starting with fatal is an OS kill
- Believes unlocking an unlocked mutex is a harmless no-op