defer, panic, and recover
Overviewβ
Go provides three interrelated mechanisms for controlling execution flow in extraordinary situations: defer, panic, and recover. Together they form Go's approach to resource cleanup and exception-like error handling β but with deliberate, explicit semantics rather than hidden control flow.
deferschedules a function call to run when the surrounding function returns, regardless of how it returns (normal return, early return, or panic). It is Go's primary tool for resource cleanup.panicsignals a programming error that cannot be handled by the caller in the usual way β it unwinds the call stack, running deferred functions as it goes.recovercatches a panic in progress and converts it back into a normal error that can be handled. It must be called inside a deferred function.
defer β Execution Order (LIFO Stack)β
Deferred calls are pushed onto a stack and executed in last-in, first-out order when the function exits. This mirrors the natural "cleanup in reverse order of acquisition" pattern.
package main
import "fmt"
func main() {
fmt.Println("start")
defer fmt.Println("defer 1 β registered first, runs last")
defer fmt.Println("defer 2")
defer fmt.Println("defer 3 β registered last, runs first")
fmt.Println("end")
}
// Output:
// start
// end
// defer 3 β registered last, runs first
// defer 2
// defer 1 β registered first, runs last
The LIFO order means that if you open resources A, B, C in that order and defer their close calls, they will close in C, B, A order β which is exactly what you want for nested resources.
package main
import "fmt"
func acquireResources() {
fmt.Println("Acquiring database connection")
defer fmt.Println("Releasing database connection")
fmt.Println("Acquiring file handle")
defer fmt.Println("Releasing file handle")
fmt.Println("Acquiring network socket")
defer fmt.Println("Releasing network socket")
fmt.Println("--- doing work ---")
}
func main() {
acquireResources()
}
// Releases in reverse order of acquisition: socket β file β db
defer + Closure Combinationβ
Deferred functions are closures β they capture variables by reference, not by value. This means a deferred function sees the final value of any variables it references, including named return values. This enables a powerful pattern: wrapping the return value with error context.
package main
import (
"errors"
"fmt"
)
// Named return value lets defer modify the returned error
func divide(a, b float64) (result float64, err error) {
defer func() {
if err != nil {
err = fmt.Errorf("divide(%v, %v): %w", a, b, err)
}
}()
if b == 0 {
err = errors.New("division by zero")
return
}
result = a / b
return
}
func main() {
if r, err := divide(10, 2); err != nil {
fmt.Println("Error:", err)
} else {
fmt.Printf("10 / 2 = %.2f\n", r)
}
if _, err := divide(5, 0); err != nil {
fmt.Println("Error:", err)
}
}
The deferred closure runs after the return statement but before the function actually returns to the caller, giving it a chance to wrap or transform the returned error.
panic and Stack Traceβ
A panic stops normal execution, immediately begins unwinding the call stack, and runs deferred functions at each level. If the panic reaches the top of the goroutine's call stack without being recovered, the program crashes with a stack trace.
package main
import "fmt"
func level3() {
panic("something went very wrong") // triggers the unwind
}
func level2() {
fmt.Println("level2: before panic")
level3()
fmt.Println("level2: this never prints")
}
func level1() {
fmt.Println("level1: before level2")
defer fmt.Println("level1: deferred β runs during unwind")
level2()
fmt.Println("level1: this never prints")
}
func main() {
defer fmt.Println("main: deferred β runs last during unwind")
level1()
fmt.Println("main: this never prints")
}
Running this produces a panic message with a full goroutine stack trace β invaluable for debugging.
When to panic:
- Index out of bounds, nil pointer dereference β the runtime panics automatically.
- A programming invariant is violated and continuing would corrupt state.
- During package initialization (
init) when a required resource is unavailable. - In test helpers (
t.Fatalfcallsruntime.Goexit, but custom helpers sometimes use panic).
When not to panic: any error the caller could reasonably handle should be returned as an error value, not a panic.
recover β Catching a Panicβ
recover stops the unwinding and returns the value passed to panic. It only works when called directly inside a deferred function. If called anywhere else, it returns nil.
package main
import (
"fmt"
"runtime/debug"
)
func safeCall(fn func()) (err error) {
defer func() {
if r := recover(); r != nil {
// Convert panic to error, optionally capture stack trace
stack := debug.Stack()
err = fmt.Errorf("recovered panic: %v\nstack:\n%s", r, stack)
}
}()
fn()
return nil
}
func riskyOperation(x int) int {
data := []int{1, 2, 3}
return data[x] // panics if x >= 3
}
func main() {
// Safe call: normal execution
err := safeCall(func() {
fmt.Println("Result:", riskyOperation(1))
})
if err != nil {
fmt.Println("Error:", err)
}
// Safe call: panic recovered
err = safeCall(func() {
fmt.Println("Result:", riskyOperation(99))
})
if err != nil {
fmt.Println("Caught panic:", err)
}
fmt.Println("Program continues normally after recovery")
}
Key constraint:recover must be called from a deferred function that is registered in the panicking goroutine. A deferred function in a different goroutine cannot catch another goroutine's panic.
Resource Cleanup Pattern with deferβ
The canonical Go resource cleanup pattern is: acquire, check error, defer release.
package main
import (
"bufio"
"fmt"
"io"
"os"
"strings"
)
func copyFile(dst, src string) error {
in, err := os.Open(src)
if err != nil {
return fmt.Errorf("open source: %w", err)
}
defer in.Close() // guaranteed to run
out, err := os.Create(dst)
if err != nil {
return fmt.Errorf("create destination: %w", err)
}
defer func() {
// Capture Close error without shadowing the main error
if cerr := out.Close(); cerr != nil && err == nil {
err = fmt.Errorf("close destination: %w", cerr)
}
}()
if _, err = io.Copy(out, in); err != nil {
return fmt.Errorf("copy data: %w", err)
}
return nil
}
func processLines(r io.Reader, process func(string)) {
scanner := bufio.NewScanner(r)
for scanner.Scan() {
process(scanner.Text())
}
}
func main() {
// Write a temp source file
src, _ := os.CreateTemp("", "src*.txt")
src.WriteString("Hello\nGo defer\npattern\n")
src.Close()
defer os.Remove(src.Name())
dst, _ := os.CreateTemp("", "dst*.txt")
dst.Close()
defer os.Remove(dst.Name())
if err := copyFile(dst.Name(), src.Name()); err != nil {
fmt.Fprintln(os.Stderr, "copyFile error:", err)
os.Exit(1)
}
// Verify the copy
f, _ := os.Open(dst.Name())
defer f.Close()
processLines(f, func(line string) {
fmt.Println("Copied:", strings.ToUpper(line))
})
}
Practical Example: HTTP Middleware Panic Recoveryβ
In an HTTP server, an unrecovered panic in a handler crashes the entire server process. A recovery middleware converts handler panics into 500 responses, keeping the server alive.
package main
import (
"fmt"
"log"
"net/http"
"runtime/debug"
"time"
)
// RecoveryMiddleware catches panics in downstream handlers
func RecoveryMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if rec := recover(); rec != nil {
stack := debug.Stack()
log.Printf("PANIC: %v\n%s", rec, stack)
http.Error(w,
"Internal Server Error",
http.StatusInternalServerError,
)
}
}()
next.ServeHTTP(w, r)
})
}
// LoggingMiddleware records request duration
func LoggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
defer func() {
log.Printf("%s %s β %v", r.Method, r.URL.Path, time.Since(start))
}()
next.ServeHTTP(w, r)
})
}
func safeHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "OK: safe handler")
}
func panicHandler(w http.ResponseWriter, r *http.Request) {
panic("intentional panic for demonstration")
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/safe", safeHandler)
mux.HandleFunc("/panic", panicHandler)
// Compose middleware: Logging wraps Recovery wraps the mux
handler := LoggingMiddleware(RecoveryMiddleware(mux))
server := &http.Server{
Addr: ":8080",
Handler: handler,
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
}
log.Println("Server starting on :8080")
log.Println(" GET /safe β normal response")
log.Println(" GET /panic β panic recovered as 500")
if err := server.ListenAndServe(); err != nil {
log.Fatal(err)
}
}
Expert Tipsβ
Deferred function arguments are evaluated immediately. Only the function body executes at defer time. Consider:
i := 0
defer fmt.Println(i) // prints 0, not the final value
i++
To capture the latest value, use a closure: defer func() { fmt.Println(i) }().
Keep recover close to the panic site. Placing recover deep in a call chain makes it hard to reason about what state has been partially modified. The best practice is to recover at the boundary of your subsystem (e.g., request handler, goroutine entry point) and re-initialize or discard any state that may be corrupt.
panic with a non-nil error value. When you must panic, panic with an error value rather than a string. This makes recovery code easier to write β the recovered value can be type-asserted to error and wrapped or logged uniformly.
Do not use defer inside a hot loop. Each defer statement allocates a deferred function record. In a loop that executes millions of times, this allocation cost adds up. Extract the loop body into a helper function so that the deferred call is registered and executed once per call, not once per iteration.