LearnAI ToolsCareerPractice BuildsPlayContact
Lesson 3020 min read

Panic, Recover & Defer

Learn how defer schedules cleanup code, how panic signals an unrecoverable error, and how recover can catch a panic before it crashes your program.

Introduction

Alongside its explicit error-value approach, Go has three special keywords for a small set of situations that fall outside normal error handling: defer for guaranteed cleanup, panic for signaling that something has gone catastrophically wrong, and recover for catching a panic before it takes down your whole program. Used well, these three tools keep resource cleanup reliable and let you contain failures at well-defined boundaries.

What You Will Learn
  • How defer schedules a function call to run later.
  • Why deferred calls run in last-in-first-out (LIFO) order.
  • How panic stops normal execution and unwinds the stack.
  • How recover can catch a panic inside a deferred function.
  • When to reach for panic instead of returning an error — and when not to.

The defer Statement

defer schedules a function call to run right before the surrounding function returns, no matter how that function exits — normally, via an early return, or even via a panic. It is most commonly used for cleanup: closing files, releasing locks, or closing network connections.

package main
import "fmt"
func greet() {
defer fmt.Println("cleaning up...")
fmt.Println("starting greet")
fmt.Println("hello, gopher")
fmt.Println("finishing greet")
}
func main() {
greet()
}
Output

Click Run to see what this code prints.

Even though defer fmt.Println("cleaning up...") appears at the top of the function, it does not run until greet is about to return. This is why defer is so useful right after acquiring a resource — you write the release code immediately next to the acquire code, and Go guarantees it will run.

defer Runs in LIFO Order

When a function has multiple defer statements, they execute in last-in-first-out order — the most recently deferred call runs first. This mirrors how you would manually unwind nested resources.

package main
import "fmt"
func main() {
fmt.Println("start")
defer fmt.Println("deferred 1")
defer fmt.Println("deferred 2")
defer fmt.Println("deferred 3")
fmt.Println("end")
}
Output

Click Run to see what this code prints.

This is especially handy when opening several resources in sequence: each defer Close() call unwinds in the reverse order of how the resources were opened, which is exactly what you want.

panic: Signaling the Unrecoverable

panic immediately stops normal execution of the current function. Any deferred calls still run, and then the panic propagates up the call stack, stopping each caller in turn, until either a recover catches it or the program crashes with a stack trace.

package main
import "fmt"
func processIndex(items []string, idx int) string {
if idx < 0 || idx >= len(items) {
panic(fmt.Sprintf("index %d out of range for slice of length %d", idx, len(items)))
}
return items[idx]
}
func main() {
items := []string{"go", "rust", "python"}
fmt.Println(processIndex(items, 1))
fmt.Println(processIndex(items, 10)) // triggers a panic
}
Output

Click Run to see what this code prints.

Go itself triggers panics for programming errors like out-of-bounds slice access, nil pointer dereferences, and division by zero on integers. These usually indicate a bug rather than an expected failure condition.

recover: Catching a Panic

recover stops a panic in its tracks — but only if called directly inside a deferred function. If the current goroutine is panicking, recover captures the value passed to panic and returns it, letting the program continue running instead of crashing.

package main
import "fmt"
func safeDivide(a, b int) (result int, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("recovered from panic: %v", r)
}
}()
result = a / b // panics if b is 0
return result, nil
}
func main() {
result, err := safeDivide(10, 2)
fmt.Println(result, err)
result, err = safeDivide(10, 0)
fmt.Println(result, err)
fmt.Println("program keeps running")
}
Output

Click Run to see what this code prints.

Notice how safeDivide converts a panic into a normal error return value using a named return and a deferred recover. This is the standard pattern for containing panics at a boundary, such as inside an HTTP handler, so one bad request cannot take down the whole server.

panic vs Returning an Error

SituationUse
Invalid user input, missing file, failed network callReturn an error
Programming bug: nil pointer, out-of-bounds index, broken invariantpanic is acceptable (or let Go's runtime panic)
A library function whose contract was violated by the callerpanic, clearly documented
Anything the caller could reasonably be expected to handleReturn an error

As a rule of thumb: if failure is expected and recoverable as part of normal operation, return an error. Reserve panic for situations that indicate a bug or a truly unrecoverable state, and always document a function if it can panic.

Common Mistakes

Avoid These Mistakes
  • Using panic/recover as a general substitute for normal error handling.
  • Calling recover outside of a deferred function, where it has no effect.
  • Forgetting that recover only stops the panic in the current goroutine — it cannot catch a panic from another goroutine.
  • Deferring expensive cleanup inside a tight loop, which delays it until the whole function returns.
  • Swallowing a recovered panic silently without logging it.

Best Practices

  • Use defer for anything that must always run, like Close() or Unlock().
  • Keep recover confined to clear boundaries, such as the top of a goroutine or an HTTP middleware.
  • Log the recovered value so failures are still visible even if the program survives.
  • Prefer returning errors for anything a caller might reasonably want to handle differently.
  • Document any exported function that can panic, so callers know what to expect.

Frequently Asked Questions

No. recover only has an effect when called directly inside a deferred function. Calling it elsewhere always returns nil.

No. os.Exit terminates the program immediately without running any deferred calls. Use it sparingly, and prefer letting main return normally.

It returns whatever value was passed to panic() — often a string or an error — or nil if there was no panic to recover from.

Key Takeaways

  • defer schedules a call to run when the surrounding function returns.
  • Multiple defers run in last-in-first-out (LIFO) order.
  • panic stops normal execution and unwinds the stack, running deferred calls along the way.
  • recover, called inside a deferred function, can stop a panic and let the program continue.
  • Use errors for expected failures and panic only for truly unrecoverable situations.

Summary

defer, panic, and recover give Go a controlled way to guarantee cleanup and to contain catastrophic failures without resorting to exceptions for everyday error handling. Use defer liberally for cleanup, reserve panic for genuinely broken states, and use recover at clear boundaries to keep one failure from crashing an entire program. Next, you will learn how Go organizes code into packages and modules.

Next Lesson →

Packages & Modules