defer, panic, and recover
Go's mechanisms for cleanup and exceptional situations — deferred execution, panics, and recovering from them.
3 min read
Go doesn't have exceptions in the try/catch sense most languages use. Instead it has three related tools: defer for guaranteed cleanup, panic for truly exceptional situations, and recover for regaining control after one.
defer: run this when the function returns
defer schedules a function call to run right before the surrounding function returns, no matter how it returns:
func readFile() {
file := openFile("data.txt")
defer file.Close()
// ... work with file ...
// file.Close() runs automatically here, even if we return early
}This is Go's answer to finally blocks — it guarantees cleanup code runs next to the resource acquisition, instead of duplicated at every return point or forgotten on an early return.
Multiple defers run in LIFO order
func demo() {
defer fmt.Println("1")
defer fmt.Println("2")
defer fmt.Println("3")
}
// prints: 3, 2, 1Deferred calls stack up and unwind in reverse order, which mirrors how you'd want nested resources cleaned up — the last thing opened is the first thing closed.
Arguments are evaluated immediately
A subtlety worth knowing: the arguments to a deferred call are evaluated when defer runs, not when the deferred call actually executes.
func demo() {
i := 1
defer fmt.Println("deferred:", i) // captures i == 1 right now
i = 2
fmt.Println("current:", i)
}
// prints: current: 2
// deferred: 1panic: stop everything, right now
panic is for situations a program genuinely cannot recover from through normal logic — a truly broken invariant, not "the user entered bad input" (that's what error is for, covered in a later lesson). Calling panic immediately stops the current function, runs any deferred calls, and propagates up the call stack until either something recovers it or the program crashes with a stack trace.
func divide(a, b int) int {
if b == 0 {
panic("division by zero")
}
return a / b
}In everyday Go, panic is rare. Idiomatic Go code returns an error value for anything a caller might reasonably want to handle, and reserves panic for programmer mistakes — a nil pointer that should never be nil, an invariant a function's own logic guarantees, an unrecoverable startup failure.
recover: catching a panic
recover stops a panic in its tracks, but it only works inside a deferred function:
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 == 0
return
}Here, dividing by zero panics, but the deferred function catches it with recover(), converts it into a normal error, and the function returns that error instead of crashing the whole program. This pattern shows up at the boundary of a system — for instance, a web server recovering from a panic in one request handler so it doesn't take down every other in-flight request — rather than scattered through everyday business logic.
The rule of thumb
Use defer constantly, for any cleanup (closing files, unlocking mutexes, closing database connections). Use error for anything expected — a file that might not exist, a network call that might fail. Reserve panic/recover for truly exceptional, non-recoverable-through-normal-flow situations, and generally only at a boundary layer. Reaching for panic the way you'd reach for throw in Java or JavaScript is one of the fastest ways to write Go code that doesn't look like Go.
Test what you just learned
4 quick questions. Get all of them right to unlock the next lesson.
You can take the quiz without an account — logging in just lets your result count toward your progress.