Go Gopher How to Go

Recover is a built-in function in Go that regains control of a panicking goroutine. It is only useful inside deferred functions and allows a program to handle a panic gracefully instead of crashing. When called during a panic, recover stops the panicking sequence and returns the value passed to panic.

💡 Key Points

  • Recover only works when called inside a deferred function
  • Returns nil if the goroutine is not panicking
  • Returns the value passed to panic if called during a panic
  • Calling recover stops the panic and resumes normal execution
  • Recover only catches panics in the same goroutine
  • Use recover to convert panics into errors at API boundaries
  • Always check if recover returned non-nil before taking action

Basic Recover Usage

Recover must be called within a deferred function to catch panics:

package main

import "fmt"

func safeDivide(a, b int) int {
    defer func() {
        if r := recover(); r != nil {
            fmt. Println("Recovered from panic:", r)
        }
    }()
    
    return a / b
}

func main() {
    result := safeDivide(10, 2)
    fmt.Println("Result:", result)
    
    result = safeDivide(10, 0)
    fmt.Println("Result after panic:", result)
    
    fmt.Println("Program continues normally")
}
Output:
Result: 5
Recovered from panic: runtime error: integer divide by zero
Result after panic:  0
Program continues normally

Converting Panic to Error

A common pattern is to convert a panic into an error return value:

package main

import (
    "fmt"
    "errors"
)

func riskyOperation(value int) (result int, err error) {
    defer func() {
        if r := recover(); r != nil {
            err = fmt.Errorf("operation failed: %v", r)
        }
    }()
    
    if value < 0 {
        panic("negative value not allowed")
    }
    
    result = 100 / value
    return result, nil
}

func main() {
    result, err := riskyOperation(10)
    fmt.Printf("Result: %d, Error:  %v\n", result, err)
    
    result, err = riskyOperation(0)
    fmt.Printf("Result: %d, Error: %v\n", result, err)
    
    result, err = riskyOperation(-5)
    fmt.Printf("Result: %d, Error: %v\n", result, err)
}
Output:
Result: 10, Error: <nil>
Result: 0, Error: operation failed: runtime error: integer divide by zero
Result: 0, Error: operation failed: negative value not allowed

Selective Recovery

You can choose to recover only specific types of panics and re-panic others:

package main

import "fmt"

type ValidationError struct {
    Message string
}

func process(value int) {
    defer func() {
        if r := recover(); r != nil {
            // Only recover from ValidationError
            if _, ok := r.(ValidationError); ok {
                fmt.Println("Recovered from validation error:", r)
            } else {
                // Re-panic for other errors
                panic(r)
            }
        }
    }()
    
    if value < 0 {
        panic(ValidationError{Message: "value must be positive"})
    }
    
    if value > 100 {
        panic("value too large") // This will not be caught
    }
    
    fmt. Println("Processing value:", value)
}

func main() {
    process(50)
    process(-10)
    fmt.Println("Done")
}
Output:
Processing value: 50
Recovered from validation error: {value must be positive}
Done

Recover Behavior

ContextReturn ValueEffect
During panic in same goroutinePanic valueStops panic, resumes execution
Not during panicnilNo effect
Called directly (not in defer)nilNo effect, panic continues
Different goroutine's panicnilCannot catch other goroutine's panic
Nested defer with recoverFirst recover catches itOnly outermost defer's recover executes

Common Recover Patterns

PatternCodeUse Case
Basic Recoverydefer func() { if r := recover(); r != nil { log.Println(r) } }()Log panic and continue
Return Errordefer func() { if r := recover(); r != nil { err = fmt.Errorf("%v", r) } }()Convert panic to error
With Stack Tracedefer func() { if r := recover(); r != nil { log.Printf("%v\n%s", r, debug.Stack()) } }()Log panic with full stack trace
Cleanup and Recoverdefer func() { cleanup(); if r := recover(); r != nil { handle(r) } }()Ensure cleanup before handling panic
Type-Based Recoverydefer func() { if r := recover(); r != nil { if err, ok := r.(MyError); ok { ... } } }()Handle specific error types
Re-panicdefer func() { if r := recover(); r != nil { cleanup(); panic(r) } }()Cleanup then propagate panic
Goroutine Safetygo func() { defer func() { recover() }(); doWork() }()Prevent goroutine panic from crashing app

Recover Use Cases

ScenarioShould Use Recover? Reason
Web server handler✅ YesPrevent one request panic from crashing server
Worker goroutine✅ YesPrevent worker crash from affecting system
API package boundary✅ YesConvert internal panics to errors for callers
Plugin/extension code✅ YesIsolate third-party code failures
Development/testing❌ NoLet panics surface to reveal bugs
Application initialization❌ NoFatal errors should stop the program
Library internal code❌ Usually NoFix bugs instead of hiding them

Recover Best Practices

PracticeDescriptionExample
Always Check for nilOnly act if recover returns non-nilif r := recover(); r != nil { ... }
Use in Defer OnlyRecover only works in deferred functionsdefer func() { recover() }()
Log Recovered PanicsAlways log when recovering from panicUse structured logging with context
Include Stack TracesCapture stack trace for debuggingdebug.Stack() in recover handler
Protect GoroutinesEvery goroutine should have recoverWrap goroutine body in defer/recover
Don't Ignore PanicsSilent recovery hides bugsAlways log or convert to error
Clean Up ResourcesEnsure cleanup before/after recoverClose files, release locks, etc.
Avoid Over-RecoveryDon't recover everywhereLet panics fail fast during development

Recover Gotchas

IssueProblemSolution
Not in Deferr := recover() returns nilAlways call recover inside defer function
Wrong GoroutineCannot catch panic from another goroutineEach goroutine needs its own recover
Nested DefersOnly one recover catches the panicFirst recover in defer chain wins
Silent Recoverydefer func() { recover() }() hides errorsAlways check and log recovered values
Re-panic Mistakespanic(recover()) may panic with nilCheck for nil before re-panicking
Resource LeaksRecovered panic may leave resources openEnsure cleanup in separate defers