Golang panic and recover: defer, stack unwinding, and catch-style recovery

Golang panic and go panic: how unwinding works with defer; golang recover from panic and go recover panic in a deferred function (go catch panic / golang catch panic pattern); panic vs errors and os.Exit; logging with debug.Stack.

Published

Updated

Read time 4 min read

Reviewed byDeepak Prasad

Golang panic and recover: defer, stack unwinding, and catch-style recovery

People search golang panic, go panic, or panic golang when something blew up at runtime; searches like go catch panic, golang catch panic, golang recover from panic, or golang panic recover point at the same mechanism: Go has no try/catch, but you can stop a panic with recover inside a defer. This article explains how unwinding interacts with deferred calls, the recover pattern (go recover panic), and when to prefer returning an error instead. For background on goroutines, see goroutines in Go and concurrency in Go.

Tested with Go 1.24 on Linux.


What golang panic does

panic stops normal execution of the current goroutine and starts stack unwinding: deferred functions run in last-in-first-out order. If nothing calls recover, the runtime prints the panic value and stack trace and the program exits (exit status 2 for many cases). Runtime errors such as indexing an empty slice or a failed type assertion can also panic implicitly.

go
package main

import "fmt"

func main() {
	fmt.Println("start")
	for i := 0; i < 5; i++ {
		if i == 3 {
			panic("stop here")
		}
		fmt.Println(i)
	}
	fmt.Println("end")
}
text
start
0
1
2
panic: stop here

goroutine 1 [running]:
...

The lines after panic in the same function do not run unless the panic is recovered.


defer during a golang panic

Deferred calls are still scheduled when their surrounding function returns—or when that function’s stack frame unwinds because of a panic. That is why cleanup (Close, Unlock, temporary file removal) belongs in defer even when failures happen.

go
package main

import "fmt"

func work() {
	defer fmt.Println("defer in work")
	fmt.Println("before panic")
	panic("boom")
	fmt.Println("after panic")
}

func main() {
	defer fmt.Println("defer in main")
	work()
	fmt.Println("main continues")
}
text
before panic
defer in work
defer in main
panic: boom
...

main continues never prints because the panic was not recovered.


golang recover from panic (go catch panic / golang catch panic)

recover returns the panic value (if any) only while a deferred function is running during unwinding from a panic on the same goroutine. Calling recover elsewhere returns nil. That is the whole story behind golang recover from panic and the usual answer to go catch panic.

go
package main

import "fmt"

func main() {
	defer func() {
		if r := recover(); r != nil {
			fmt.Println("recovered:", r)
		}
	}()
	panic("boom")
	fmt.Println("never reached")
}
Output

You should see recovered: boom and the program exit successfully; the line after panic is still skipped.

Logging with runtime/debug.Stack

To log a golang panic to stderr or a file, capture the value and stack inside the deferred recover:

go
package main

import (
	"log"
	"runtime/debug"
)

func risky() {
	defer func() {
		if r := recover(); r != nil {
			log.Printf("panic: %v\n%s", r, debug.Stack())
		}
	}()
	panic("example")
}

func main() {
	risky()
	log.Println("after risky")
}

For richer logging setups, see Logrus tutorial or changing the default log format.


panic versus returning errors and os.Exit

For expected failures (bad input, I/O errors), return an error and document invariants instead of panicking. Reserve panic for bugs or states you truly cannot continue from inside a package boundary.

os.Exit terminates the process immediately without running deferred functions in other frames; use it sparingly (for example in main after printing help, or in tiny tools). Panic runs defers until recover or program death—see Effective Go — Panic and Defer, Panic, and Recover.


Summary

Golang panic starts unwinding the current goroutine’s stack and runs deferred work until the program exits or a deferred function calls recover. That is how go recover panic and golang panic recover work; there is no separate go catch panic keyword—only the defer-plus-recover idiom. recover is scoped to the panicking goroutine and must run from a deferred call. Prefer error returns for normal control flow; use panic rarely, and understand that os.Exit skips deferred cleanup.


References


Frequently Asked Questions

1. Is there try/catch in Go for golang catch panic?

No. Use defer with an anonymous function that calls recover() to stop a panic in the same goroutine; that is the idiomatic catch-style pattern.

2. Where does recover work in Go?

Only inside a deferred function that runs while the stack is unwinding from a panic, on the same goroutine that panicked. It does not catch panics in other goroutines.

3. When should I use panic instead of returning an error?

Reserve panic for programming mistakes or unrecoverable internal state; for expected failures, return error and let callers decide. Libraries should almost never panic across API boundaries.

4. What is the difference between panic and os.Exit?

panic unwinds the current goroutine and runs deferred functions until recover stops it or the program exits. os.Exit skips deferred cleanup and ends the process immediately with an exit code.
Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with more than 15 years of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive …