Recover
Recover from panics in deferred functions, re-panicking, and converting panics to errors at package boundaries.
Recover is a built-in that catches panics in a deferred function. When called inside a defer, recover returns the value passed to panic; called outside a defer, it returns nil. Recover is used at package boundaries to convert panics to errors (e.g., in HTTP handlers) or to log a panic with context before crashing.
Recover inside a deferred function catches a panic and returns the panic value. If no panic is in flight, recover returns nil.
package main
import (
"fmt"
)
func main() {
defer func() {
if r := recover(); r != nil {
fmt.Println("recovered:", r)
}
}()
fmt.Println("before panic")
panic("something went wrong")
fmt.Println("never reached")
}
// Output:
// before panic
// recovered: something went wrongSafe divide: a function that panics on division by zero. A caller can recover to handle the error gracefully.
package main
import (
"fmt"
)
func safeDivide(a, b int) (result int, err error) {
defer func() {
if r := recover(); r != nil {
// Convert panic to error
err = fmt.Errorf("divide panic: %v", r)
result = 0
}
}()
if b == 0 {
panic("divide by zero")
}
return a / b, nil
}
func main() {
res, err := safeDivide(10, 2)
fmt.Println(res, err) // 5 <nil>
res, err = safeDivide(10, 0)
fmt.Println(res, err) // 0 divide panic: divide by zero
}Re-panicking: a defer that logs a panic and re-panics to propagate it up the stack.
package main
import (
"fmt"
"runtime/debug"
)
func criticalOperation() {
defer func() {
if r := recover(); r != nil {
fmt.Println("CRITICAL ERROR:", r)
fmt.Println("Stack trace:")
fmt.Println(string(debug.Stack()))
panic(r) // re-panic
}
}()
// Simulate a critical failure
panic("database connection lost")
}
func main() {
// The program will crash because criticalOperation re-panics
criticalOperation()
}HTTP handler that recovers panics and converts them to 500 responses. This is the standard pattern in web frameworks - panic in a handler is caught at the framework level, logged with stack trace, and a 500 is sent to the client.
package main
import (
"fmt"
"log"
"net/http"
"runtime/debug"
)
func recoverMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
log.Printf("PANIC in %s %s: %v\n", r.Method, r.RequestURI, err)
log.Printf("Stack:\n%s", debug.Stack())
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprintf(w, "Internal Server Error")
}
}()
next.ServeHTTP(w, r)
})
}
func panicHandler(w http.ResponseWriter, r *http.Request) {
panic("deliberate panic")
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/panic", panicHandler)
handler := recoverMiddleware(mux)
log.Println("server listening on :8080")
log.Fatal(http.ListenAndServe(":8080", handler))
}In production
Recover is primarily for converting panics to errors at package boundaries (HTTP handlers, RPC servers, goroutine launches). Never silently swallow a panic - always log the error with a stack trace (use runtime/debug.Stack()) before converting to an error return or sending a response. Never recover inside a goroutine that you launched unless that goroutine is a long-lived service; panics in background goroutines that you don't recover will crash the process. The pattern: HTTP handler or RPC framework catches panic → logs with stack trace → responds with 500 or error → continues serving. Panics that reach the main goroutine should crash the process (use panic for programmer errors, not expected failures). If you find yourself recovering the same panic in multiple places, it's a sign that the panic should have been an error return instead.
Enjoyed this? Get more essays on software craft delivered to your inbox.
Subscribe free