Last Updated: May 22, 2026
panic is Go's mechanism for aborting normal flow when a program hits a state it cannot continue from. It's not the everyday error-handling tool, that's the error interface we covered in earlier chapters. Panics exist for programmer mistakes and impossible situations: dereferencing a nil pointer, indexing out of range, dividing by zero, sending on a closed channel. This chapter covers what panics are, what triggers them, how they unwind the call stack, how defer interacts with them, and the rare cases where calling panic yourself makes sense.
A panic is a runtime mechanism that stops normal execution and starts unwinding the stack. Each function on the stack pops in turn, running its deferred calls, until either something catches the panic with recover or the program crashes and prints a stack trace.
Two different things can start a panic. The Go runtime raises one when a program does something the language considers a bug at runtime. Your own code can raise one with the built-in panic function. The mechanism is identical from that point on.
Here's a runtime panic in its simplest form:
The slice has two elements, indices 0 and 1. Reading index 5 has no defined meaning, so the runtime panics. The program prints the panic message, the goroutine that panicked, the function and line where the panic originated, and exits with status 2. That format, the word panic: followed by the reason, the goroutine block, and the stack frames, is what you'll see for every uncaught panic.
The phrase "runtime error" in the message tells you the panic came from the runtime, not from a user panic(...) call. Errors raised by user code look different, as we'll see later.
This is the most important distinction in the chapter. The two are not interchangeable, and treating them as if they were leads to brittle programs.
An error is a value a function returns to signal an expected failure path. A file might not exist. A network call might time out. A user might submit an invalid coupon code. These are situations the caller is expected to handle, so the function reports them through its return values and lets the caller decide what to do.
A panic is for situations the caller is not expected to handle, because the program has hit a state that violates an assumption baked into the code itself. Indexing past the end of a slice is not a "user error", it's a bug. Dereferencing a nil pointer is not something you handle case-by-case, it means somebody didn't initialize a value they were supposed to.
| Scenario | Use error | Use panic |
|---|---|---|
| File not found | Yes | No |
| User submitted invalid input | Yes | No |
| Database connection refused | Yes | No |
| Coupon code expired | Yes | No |
| Index out of range on a slice you control | No | Yes (runtime raises it) |
| Nil pointer where the contract said non-nil | No | Yes (runtime raises it) |
| Unreachable branch ("this can never happen") | No | Yes (you raise it) |
| Required configuration missing at startup | Maybe | Sometimes (program can't run) |
The rule of thumb: if the caller can do something useful when this happens, return an error. If the only sane response is to crash because the program has lost the ability to function correctly, panic.
The diagram walks through the decision. If the caller has any reasonable way to recover (retry, fall back, ask the user, log and skip), return an error. Only when the program has reached a state where continuing would be wrong should you panic.
The Go runtime raises panics for a small, fixed set of conditions. Recognizing them on sight saves a lot of debugging time, because the message tells you exactly what went wrong.
p.Code is shorthand for (*p).Code. Since p is nil, the runtime detects the bad memory access and panics. This is the most common runtime panic. The line panic: runtime error: invalid memory address or nil pointer dereference is what you'll see.
The runtime checks the bounds on every slice or array index. If the index is negative or greater than or equal to the length, it panics. The message includes both the offending index and the slice's length, which is usually enough to pinpoint the bug.
Slicing past the capacity panics. Note that the message says capacity, not length, because slicing can extend beyond the current length as long as the capacity allows it. This catches a class of bugs you don't get from plain indexing.
Integer division by zero panics. The same happens for the % operator. Floating-point division by zero does not panic, it produces +Inf, -Inf, or NaN depending on the numerator, because the IEEE 754 specification defines those results.
The single-return form of a type assertion (x.(T)) panics if x doesn't actually hold a T. The two-return form (v, ok := x.(T)) doesn't panic, it sets ok to false and v to the zero value of T.
Once a channel is closed, sending on it panics. Receiving from a closed channel does not panic, it returns the zero value of the channel's element type along with ok == false. Closing an already-closed channel also panics. This is one of the few runtime panics that's easy to trigger from concurrent code by accident.
Go's map implementation includes a runtime check that crashes the program if it detects two goroutines writing to the same map at the same time. The message says fatal error rather than panic: because this particular check is not recoverable, even with recover. Use a sync.Mutex or sync.Map for concurrent access.
A nil map is safe to read but not to write. Writing to one panics with this specific message. The fix is to construct the map with make first, or with a map literal.
These are the runtime panics you'll see most often. The full list in the Go specification is longer (it includes things like calling a method on a nil interface), but these cover the cases that come up in practice.
The built-in panic function takes any value (its parameter type is interface{}, also written any) and starts a panic with that value as the panic argument.
The message format mirrors a runtime panic, but the body comes from whatever you passed. Strings are common because they're easy to read in the trace, but errors are more useful because they compose better with recover and existing error inspection tools:
The output looks similar because errors.New produces a value whose Error() method returns the message, and the runtime calls that method when formatting the panic. The difference matters at recovery time. With an error, code that catches the panic with recover can call errors.Is or errors.As on it, which is much more useful than parsing a string.
You can panic with any value at all. A struct, a number, a custom type. In practice almost nobody does, because strings and errors cover the cases people actually need, and stranger panic values just make the trace harder to read.
The panic argument is accessible through recover. For now, the important property is that whatever you pass goes into the stack trace and is available if anyone catches the panic.
When a panic starts, normal execution stops. The function where the panic originated does not finish its remaining statements. Instead, the runtime begins unwinding the call stack frame by frame, from the panicking function back toward main.
Each frame that pops runs its deferred calls in last-in-first-out order. If none of those deferred calls recover, the unwinding continues to the next frame up. If the unwinding reaches the top of the goroutine without anyone recovering, the program prints a stack trace and exits.
Notice what didn't run. chargeCustomer panicked before its second fmt.Println. processOrder's second fmt.Println didn't run either because the panic interrupted the call to chargeCustomer. The same for main's last fmt.Println. Everything after the panic, up the stack, is skipped.
The trace shows the stack in panic order: the panicking function at the top, then its caller, then its caller's caller, all the way to main. Reading from top to bottom tells you exactly how the program got to the panic site.
The diagram traces the unwind. The panic enters at chargeCustomer, propagates back through processOrder, then through main. At each level the remaining statements are skipped. With no deferred function to catch the panic, the goroutine terminates and the program exits.
The compiler also issues a warning for the fmt.Println("end chargeCustomer") line in the example above, because it's unreachable after an unconditional panic. Real programs rarely have unreachable code right after a panic; the panic usually sits inside a conditional that the compiler can't statically determine.
Here's the part that makes panics useful for cleanup. As each function on the stack unwinds, its deferred calls still run. That guarantee is the entire reason patterns like deferred file close and deferred mutex unlock are safe even when something blows up.
Every deferred call ran, in reverse order of where they were registered: chargeCustomer's defer first, then processOrder's, then main's. The panic message comes after them because the runtime prints the trace only once the stack has fully unwound. If you ever wondered whether defer file.Close() runs on a panic path, the answer is yes.
The same applies for multiple deferred calls within one function, which fire in LIFO order regardless of whether the function returns normally or unwinds because of a panic:
The three deferred prints run in reverse registration order, just like they would on a normal return. The panic doesn't change the rules of defer, it just triggers the unwind differently than a return would.
The diagram shows the LIFO order. Defers register in order 1, 2, 3 during normal execution. When the panic fires, they unwind in reverse: 3, then 2, then 1. Only after all of them run does the panic move up to the caller's frame.
A practical example with a file handle. The deferred Close runs even when the panic comes from somewhere unexpected:
The file gets closed even though we never reach a normal return. That's the property that makes patterns like defer mu.Unlock() safe in the presence of panics. If a mutex-holding function panics, the unlock still runs, and the mutex is released rather than left locked forever. recover only works when called from inside a deferred function, which is also a direct consequence of how unwinding interacts with defer.
Cost: every defer registers a small piece of bookkeeping that fires at function exit, whether the exit is a return or an unwind. The cost is a few nanoseconds per defer. In a hot loop running a million times you'd notice; in any normal function it's irrelevant.
Go has another stack-unwinding mechanism that looks superficially like panic but is meant for a different job. runtime.Goexit terminates the current goroutine cleanly. Like a panic, it runs all deferred functions on its way out. Unlike a panic, it doesn't print a stack trace, it doesn't propagate as an error, and recover doesn't stop it.
Goexit ended the worker goroutine while still running its deferreds. The main goroutine is untouched, no stack trace is printed, and the program exits with status 0. Compare that to a panic("...") in the worker, which would propagate up the worker's stack and, if not caught, would crash the whole program.
Calling runtime.Goexit from the main goroutine has a special wrinkle: it runs all deferreds on main, then terminates the goroutine, which is enough to end the program. Most code never needs Goexit. The two places it shows up in practice are inside the testing package (where t.FailNow calls it internally to terminate the current test goroutine) and in code that wants a goroutine to bail out without disturbing anything else. For everyday code, return is what you want.
The summary: panic is for unrecoverable errors and propagates as an error; Goexit is for clean goroutine termination and doesn't.
The default answer is "almost never". Most situations that look like they want a panic are better served by an error return value. There are a few cases, though, where calling panic directly is appropriate.
Init failures that make the program unrunnable. Inside an init function, there's no caller to return an error to. The package itself can't continue. Panicking here is reasonable because the failure mode is "the program cannot start":
If the regex is malformed, the program panics during init and never enters main. The standard library does this too: regexp.MustCompile panics on a bad pattern, precisely so package-level variables can be initialized without checking errors.
The same pattern with a built-in helper from the standard library:
MustCompile calls Compile internally and panics if it returns an error. The "Must" prefix is a convention you'll see throughout the standard library (template.Must, regexp.MustCompile) for helpers that turn a returned error into a panic. The contract is "use this only with inputs that are guaranteed to be valid at compile time".
Impossible branches. Sometimes a code path is structurally unreachable, the compiler just doesn't know it. Putting a panic in that branch documents the invariant and crashes loudly if the invariant is ever broken:
The switch covers every defined value of OrderStatus. If someone later adds a new status and forgets to update describe, the panic fires the first time that value reaches the function. A silent default that returns "" would let the bug hide for a long time; the panic surfaces it immediately.
Programmer-error contracts. When a function's contract specifies that a precondition must hold and violating it indicates a bug in the caller (not a runtime condition), panicking is one valid response. Stack operations on an empty stack, slice operations with negative indices, calling methods on objects in invalid states. These are bugs, not failure modes.
This is a stylistic choice. A stack like this could also return (string, bool) or (string, error) and let the caller decide. The trade-off is between "calling Pop on an empty stack is a bug in the caller, fail loudly" (panic) and "emptiness is an expected condition, let the caller check" (error or ok). The standard library makes both choices in different places: sync.Mutex.Unlock on an unlocked mutex panics (it's a bug); map[k] returns a zero value rather than panicking on missing keys (it's a normal condition).
The default should still be "return an error". Panic is the exception, not the rule, and the cases above are deliberately narrow.
There's a guideline in idiomatic Go that library code should not panic for conditions a caller might reasonably encounter. The reasoning is straightforward: a library doesn't know what the caller is doing. A web service handling thousands of requests per second cannot afford to crash because one request fed bad data into a library helper. The library's job is to report problems through errors and let the caller make the policy decision about how to respond.
A few practical consequences flow from this:
MustCompile, MustParse) exist for the narrow case where the caller has a literal value at compile time and the failure can only mean a programmer mistake.recover, which defeats the point of Go's error model.The principle is: panic crashes the program by default, and a well-designed library doesn't use it except in the narrow cases above.
Every uncaught panic prints a stack trace. Knowing how to read it cuts debugging time substantially.
The trace reads top-down from the panic site outward to main. Line by line:
panic: runtime error: index out of range [0] with length 0 is the panic argument. The runtime synthesizes this for built-in panics; user code's argument would appear here verbatim.goroutine 1 [running]: says which goroutine panicked and its state. In a multi-goroutine program you may see multiple goroutine blocks; the first one is the one that panicked.main.(*Cart).FirstItem(...) is the function on top of the stack, with (*Cart) showing it's a method on *Cart. The line below is the file and line number.main.describeCart(...) is the caller.main.main() is the bottom of the goroutine's stack. The +0x39 is an offset within the compiled function, not usually useful to a Go programmer.The first frame after the panic message is the actual culprit. Walking down shows you how the program got there. For runtime panics, the message itself usually tells you exactly what went wrong (an out-of-range index, a nil dereference, a closed channel). For user panics, the message is whatever you passed to panic(...).
For panics from a non-main goroutine, the trace is similar but the goroutine number isn't 1, and the program still exits unless something recovers:
A panic in a child goroutine, with no recover, crashes the whole program. Panicking in a goroutine isn't isolated from the rest of the program the way an exception in a thread might be in some other languages. Every goroutine shares the same process, and an uncaught panic from any of them ends the process. This is one reason long-running services often wrap goroutine entry points with deferred recover.