Panic

How the built-in panic function immediately halts normal control flow, runs deferred calls in order, unwinds the stack, and terminates the program – and when to use it

When a Go program reaches a state where further execution is impossible or would produce corrupt results, it can panic. A panic is a deliberate (or accidental) signal that stops the ordinary flow of the current function, triggers every defer still pending in the call stack, and eventually crashes the program unless a recover call intervenes. Unlike error returns, a panic is not a routine communication channel — it is an emergency exit.

Think of it this way: error values are the conversation your code has with its caller about things that went wrong but can be handled. A panic is the fire alarm — you pull it only when staying inside the building is worse than leaving.


What the panic Function Actually Does

The built-in function panic accepts a single argument of any type (the “panic value”) and never returns in the normal sense. Instead, it kicks off a fixed sequence of events that unwinds the current goroutine’s stack.

Only the current goroutine panics:

A panic travels up the stack of the goroutine that called panic. It does not cross goroutine boundaries. If any goroutine panics and the panic is not recovered, the entire program terminates — but the other goroutines receive no direct notification.

Here is the guaranteed order when a function F calls panic:

  1. Execution of F stops immediately.
  2. All deferred calls inside F run, in last-in-first-out (LIFO) order.
  3. Control returns to F’s caller, which then behaves as though it called panic.
  4. Steps 1‑3 repeat for each level up the call stack.
  5. If the panic reaches the top of the goroutine (the function that started the goroutine) without being recovered, the runtime prints the panic value and a full stack trace, then terminates the program with a non‑zero exit code.

A Minimal Example

package main
import "fmt"
func main() {
	fmt.Println("Before panic")
	panic("something went badly wrong")
	fmt.Println("After panic") // never executed
}

Running this program produces:

Before panic
panic: something went badly wrong
goroutine 1 [running]:
main.main()
        /tmp/sandbox/main.go:7 +0x...
exit status 2

The message After panic never prints. The panic call acts as an immediate, unconditional transfer of control — the function that panics does not finish, and neither does any caller above it unless a deferred recover catches the panic. The printed trace shows exactly which line triggered the panic, which is invaluable during debugging.


Implicit Panics from Runtime Errors

You do not always need to call panic yourself. The Go runtime triggers panics automatically when it encounters operations that cannot proceed safely. These are still full panics — the unwinding mechanism is identical.

Common implicit panics include:

  • nil pointer dereference — calling a method or accessing a field on a nil pointer.
  • out‑of‑bounds slice or array accesss[5] when len(s) is 3.
  • integer division by zerox / 0 where x is an integer type.
  • sending on a closed channelch <- val after close(ch).
  • type assertion failurev := i.(string) when i does not hold a string (without the comma‑ok form).
package main
import "fmt"
func triggerRuntimePanic() {
	nums := []int{10, 20, 30}
	fmt.Println(nums[4]) // index out of range
}
func main() {
	fmt.Println("Starting")
	triggerRuntimePanic()
	fmt.Println("Finished") // never reached
}

Output:

Starting
panic: runtime error: index out of range [4] with length 3
goroutine 1 [running]:
main.triggerRuntimePanic()
        /tmp/sandbox/main.go:7 +0x...
main.main()
        /tmp/sandbox/main.go:12 +0x...
exit status 2

The runtime prefixes the message with runtime error: so you can distinguish an implicit panic from an explicit call. The stack unwinding and deferred‑function execution are exactly the same as for panic("some value").

Nil pointer dereferences are the most common implicit panic:

A nil pointer dereference does not produce an error value you can check — it crashes the program. Always validate that a pointer is non‑nil before using it, or design functions so that nil receivers are explicitly handled (where that makes sense for the API).


Stack‑Unwinding and Deferred Calls

The panic sequence is not a single jump to the top of the goroutine. It walks up the stack frame by frame, and before leaving each function it dutifully runs every deferred call that function registered. This is the mechanism that allows you to clean up resources (close files, release mutexes) even when a function is terminated abnormally.

Understanding the order is essential: the innermost deferred functions run first, then the caller’s deferred functions, and so on.

package main
import "fmt"
func a() {
	defer fmt.Println("defer in a")
	b()
	fmt.Println("after b in a") // never reached
}
func b() {
	defer fmt.Println("defer in b")
	c()
	fmt.Println("after c in b") // never reached
}
func c() {
	defer fmt.Println("defer in c")
	panic("panic from c")
}
func main() {
	a()
}

Output:

defer in c
defer in b
defer in a
panic: panic from c
...

The panic begins in c. Before c returns to b, it runs defer in c. When control reaches b, b behaves as if it panicked itself — it runs defer in b, then returns to a. a runs defer in a, and then the panic reaches main and terminates.

The deferred function calls ran in reverse order of their declaration: c, then b, then a. This LIFO guarantee is what makes defer reliable for cleanup pairs — you acquire a resource, then immediately defer its release, and that release will always happen in the correct reverse order.

Deferred functions always run during a panic – do not rely on them to ‘fix’ the panic:

Deferred calls run regardless of how the function exits (normal return, panic, or even a runtime error). However, a deferred function cannot magically resume the function that panicked; it can only clean up resources or, if it calls recover, stop the panic from propagating further. Without a recover, the function still exits via the panic path.


When to Use panic (and When Not To)

Panic is not an alternative to returning an error. Returning an error is a conversation; panic is an evacuation. Use panic only when the program genuinely has no safe way to continue.

Legitimate uses of panic

  • Startup failures that make the program unusable: a web server that cannot bind to its configured port, a database connection that cannot be established, missing mandatory configuration values. There is no value in trying to limp forward.
  • Programmer errors that indicate a bug: passing a nil pointer to a function that documents it must be non‑nil, violating an invariant that the surrounding code guarantees. In these cases, a loud crash with a clear stack trace is better than silently corrupting state.
func NewServer(addr string) *http.Server {
	if addr == "" {
		panic("server address must not be empty")
	}
	return &http.Server{Addr: addr}
}

Panic at startup is a clear, debuggable signal:

If your service cannot start because a port is occupied, a panic with a stack trace pointing directly to the ListenAndServe call gives the operator immediate clarity. There is no need to wrap that in an error return that might be ignored.

Where panic does not belong

  • Expected, recoverable errors: a file not found, a network timeout, invalid user input. These belong in the normal error‑return path.
  • Library code that decides the caller’s fate: a library function should almost never panic. It should return an error and let the caller decide whether the situation is fatal.
  • Control flow in the middle of normal operation: panic is not a substitute for break, return, or goto. It is heavier and far more disruptive.

Common Mistakes and Misunderstandings

Treating panic like exceptions in other languages

In some languages, exceptions are the primary error‑handling mechanism; they are thrown routinely and caught somewhere up the stack. In Go, errors are values returned from functions. Panic is reserved for situations that truly cannot be recovered, and recover exists mostly to protect library boundaries — for example, a recursive descent parser that uses panic to unwind from a deeply nested error and then returns a clean error to the caller.

Forgetting that deferred functions still run

A panic does not skip defer; it runs every deferred call in LIFO order. If you acquire a mutex and then panic before releasing it, the deferred Unlock() will still execute, preventing a deadlock. This is by design, but it also means that a panic in the middle of a partially‑constructed data structure will trigger all the deferred cleanup — including closing files and releasing locks — possibly while invariants are broken.

func updateMap(m map[string]int, key string) {
	mu.Lock()
	defer mu.Unlock()
	// Simulate an invariant check that fails
	if m[key] < 0 {
		panic("negative value not allowed")
	}
	m[key]++
}

If the panic fires, mu.Unlock() is called, so the lock is not leaked. However, the map may be left in an indeterminate state (the value was negative, which should not have been possible). That is acceptable only if the program is about to terminate anyway.

Assuming panic will always crash the whole program immediately

A panic only crashes the program if it escapes to the top of the goroutine’s call stack without being recovered. If somewhere on the stack a deferred function calls recover, the panic stops and normal execution resumes from the point after the function that called recover. The key lesson now: panic is not guaranteed to be the end — it is an event that travels upward and can be intercepted.

Never panic across API boundaries you do not control:

If your function calls user‑supplied callbacks, a panic inside that callback could travel back through your code. Use recover at the boundary to convert the panic into an error, protecting your program’s stability.


How Beginners Should Think About Panic

Imagine a factory assembly line. Each station does a job and passes the product to the next station. If a station detects that a part is cracked and continuing will destroy the machine, it pushes the emergency stop button. That button does not try to fix the part — it shuts everything down in a controlled order (powering off tools, retracting arms) and then the whole line stops.

The emergency stop is panic. The controlled shutdown is the execution of deferred functions. The line supervisor who may decide to restart the line and write an incident report is recover. Until you study recover, think of panic as a definitive, permanent halt — because without recover, that is exactly what it is.


Summary

panic is Go’s mechanism for abandoning the current control flow when the program has reached a dead end. It immediately halts the panicking function, runs all pending deferred calls in the correct reverse order, and propagates the panic up the call stack until the program exits. Runtime errors (nil dereferences, slice bounds) invoke the same machinery.

The important insight is that panic and defer are two halves of a single design: defer guarantees that cleanup code will execute even in catastrophic exits, and panic ensures that when a function cannot meet its contract, it does not silently return garbage or corrupt state. This pairing is what makes it safe to write functions that acquire resources and then panic — the resources will still be released.