Skip to content

Go Panic and Recover — defer panic recover and Safe Error Handling Explained

DodaTech Updated 2026-06-28 9 min read

In this tutorial, you will learn about Go Panic and Recover. We cover key concepts, practical examples, and best practices to help you master this topic.

Go panic and recover handle exceptional conditions with defer for guaranteed cleanup, panic for unrecoverable errors, and recover for catching panics to prevent Goroutine crashes.

What You'll Learn

  • Using defer for cleanup operations
  • When to use panic vs errors
  • Recovering from panics safely
  • Stack trace analysis

Why It Matters

Panics crash your program if not recovered. Docker uses recover in HTTP handlers. Kubernetes uses defer extensively for resource cleanup. DodaZIP uses recover to handle corrupt archive panics gracefully.

Real-World Use

HTTP middleware recovers panics per-request. Database connection cleanup uses defer. Background job processors recover panics per-job. File operations use defer for closing.

flowchart LR
    A["Panic/Recover"] --> B["defer"]
    B --> C["panic"]
    C --> D["recover"]
    D --> E["Stack Trace"]
    A:::current --> B
    style A fill:#2563eb,stroke:#2563eb,color:#fff
    style B fill:#dbeafe,stroke:#2563eb,color:#1e40af
    style C fill:#dbeafe,stroke:#2563eb,color:#1e40af
    style D fill:#dbeafe,stroke:#2563eb,color:#1e40af
    style E fill:#f1f5f9,stroke:#94a3b8,color:#64748b

defer

defer schedules a function call to run when the surrounding function returns:

func readFile(path string) ([]byte, error) {
    f, err := os.Open(path)
    if err != nil {
        return nil, err
    }
    defer f.Close()  // Runs when readFile returns

    return io.ReadAll(f)
}

Multiple defers (LIFO order)

func cleanup() {
    defer fmt.Println("third")
    defer fmt.Println("second")
    defer fmt.Println("first")

    fmt.Println("body")
}
// Output:
// body
// first
// second
// third

defer with closures

func count() {
    n := 0
    defer func() {
        fmt.Println("Final count:", n)  // Captures n by reference
    }()

    for i := 0; i < 5; i++ {
        n++
    }
    // Final count: 5
}

defer with return values

func double(x int) (result int) {
    defer func() {
        result *= 2  // Modifies named return value
    }()
    return x
}

func main() {
    fmt.Println(double(5))  // 10
}

panic

Panic stops normal execution and begins unwinding the stack:

func main() {
    fmt.Println("start")
    panic("something bad happened")
    fmt.Println("end")  // Never reached
}
// Output:
// start
// panic: something bad happened

Common panic causes

// Index out of range
var s []int
_ = s[0]  // panic: runtime error: index out of range

// Nil pointer dereference
var p *int
*p = 42  // panic: runtime error: invalid memory address

// Type assertion failure
var i interface{} = "hello"
n := i.(int)  // panic: interface conversion: string is not int

// Division by zero
_ = 1 / 0  // panic: runtime error: integer divide by zero

// Close closed channel
ch := make(chan int)
close(ch)
close(ch)  // panic: close of closed channel

recover

recover catches a panic and returns the panic value:

func safeDivide(a, b int) (result int, err error) {
    defer func() {
        if r := recover(); r != nil {
            err = fmt.Errorf("panic: %v", r)
        }
    }()
    return a / b, nil
}

func main() {
    result, err := safeDivide(10, 0)
    if err != nil {
        fmt.Println("Error:", err)
    } else {
        fmt.Println("Result:", result)
    }
}

HTTP Middleware Pattern

func RecoveryMiddleware(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 recovered: %v\n%s", err, debug.Stack())
                http.Error(w, "Internal Server Error", http.StatusInternalServerError)
            }
        }()
        next.ServeHTTP(w, r)
    })
}

Background Job Recovery

func processJob(job Job) (err error) {
    defer func() {
        if r := recover(); r != nil {
            err = fmt.Errorf("job %d panicked: %v", job.ID, r)
            log.Printf("Recovered from panic in job %d: %v", job.ID, r)
        }
    }()

    // Job processing logic
    return process(job)
}

panic with error conversion

// Must* pattern — panic on failure (for initialization)
func MustParse(raw string) time.Time {
    t, err := time.Parse(time.RFC3339, raw)
    if err != nil {
        panic(fmt.Sprintf("MustParse(%q): %v", raw, err))
    }
    return t
}

// Usage in package-level initialization
var defaultTime = MustParse("2026-01-01T00:00:00Z")

// regexp.MustCompile uses this pattern
var validID = regexp.MustCompile(`^[a-z]+\[[0-9]+\]$`)

Stack Traces

func a() { b() }
func b() { c() }
func c() { panic("in c") }

func main() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Recovered:", r)
        }
    }()
    a()
}
// Output:
// Recovered: in c

Getting the stack trace

import "runtime/debug"

func safeCall(fn func()) {
    defer func() {
        if r := recover(); r != nil {
            log.Printf("panic: %v\n%s", r, debug.Stack())
        }
    }()
    fn()
}

Best Practices

// 1. Use error returns for expected failures, not panic
func divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, errors.New("division by zero")
    }
    return a / b, nil
}

// 2. Use defer paired with the resource acquisition
f, err := os.Open(name)
if err != nil {
    return err
}
defer f.Close()  // Right after open

// 3. Only use panic for truly exceptional cases
// Programmer errors, unrecoverable states, init failures

// 4. Always recover in goroutines
go func() {
    defer func() {
        if r := recover(); r != nil {
            log.Printf("goroutine panicked: %v", r)
        }
    }()
    doWork()
}()

// 5. Document when a function panics
// MustParse panics if the string is not a valid time.

Common Mistakes

1. Recover Outside defer

// Bad — recover must be in defer
func bad() {
    recover()  // Returns nil, doesn't catch anything
    panic("die")
}

// Good
func good() {
    defer func() { recover() }()
    panic("die")
}

2. Not Recovering in Goroutines

// Bad — goroutine panic crashes entire program
go func() {
    doRiskyWork()  // Panic here kills the whole process!
}()

// Good
go func() {
    defer func() {
        if r := recover(); r != nil {
            log.Printf("goroutine panic: %v", r)
        }
    }()
    doRiskyWork()
}()

3. Using panic for Input Validation

// Bad — panic for user input errors
func process(input string) {
    if input == "" {
        panic("input cannot be empty")
    }
}

// Good — return error
func process(input string) error {
    if input == "" {
        return errors.New("input cannot be empty")
    }
    return nil
}

4. defer in a Loop

// Bad — defers accumulate, don't run until function returns
for _, f := range files {
    f, _ := os.Open(f)
    defer f.Close()  // All files stay open until loop exits!
}

// Good — close explicitly or use anonymous function
for _, f := range files {
    func() {
        f, _ := os.Open(f)
        defer f.Close()  // Runs at end of anonymous function
        // process f
    }()
}

5. Ignoring defer Order

// Bad — two defers in wrong order
resp, err := http.Get(url)
defer resp.Body.Close()  // If err != nil, resp is nil — panic!
if err != nil {
    return err
}

// Good — check error before defer
resp, err := http.Get(url)
if err != nil {
    return err
}
defer resp.Body.Close()

Practice Questions

1. When does a deferred function run?

When the surrounding function returns (normally or via panic). defers are guaranteed to run even if the function panics.

2. What does recover return?

The value passed to panic, or nil if there's no active panic. Only useful inside a deferred function.

3. When should you use panic instead of error?

When the program cannot continue: programmer errors (nil pointer, index bounds), failed package-level initialization, impossible conditions.

4. What happens if a deferred function panics?

The new panic propagates, potentially replacing the original panic. All remaining defers still run.

Challenge: Build a SafeExecutor that runs functions with timeout, recovery, and error handling, suitable for a job processing system.

Solution
package main

import (
    "fmt"
    "runtime/debug"
    "time"
)

type SafeExecutor struct {
    Timeout time.Duration
}

type JobResult struct {
    Output interface{}
    Err    error
}

func (e *SafeExecutor) Execute(fn func() interface{}) (result JobResult) {
    // Recovery
    defer func() {
        if r := recover(); r != nil {
            result = JobResult{Err: fmt.Errorf("panic: %v\n%s", r, debug.Stack())}
        }
    }()

    // Timeout
    done := make(chan JobResult, 1)
    go func() {
        defer func() {
            if r := recover(); r != nil {
                done <- JobResult{Err: fmt.Errorf("goroutine panic: %v", r)}
            }
        }()
        done <- JobResult{Output: fn()}
    }()

    select {
    case result = <-done:
        return
    case <-time.After(e.Timeout):
        return JobResult{Err: fmt.Errorf("timeout after %v", e.Timeout)}
    }
}

func main() {
    executor := &SafeExecutor{Timeout: 100 * time.Millisecond}

    // Successful execution
    result := executor.Execute(func() interface{} {
        return "hello"
    })
    fmt.Println("Success:", result.Output, result.Err)

    // Panic recovery
    result = executor.Execute(func() interface{} {
        panic("something broke")
    })
    fmt.Println("Panic:", result.Err != nil)

    // Timeout
    result = executor.Execute(func() interface{} {
        time.Sleep(200 * time.Millisecond)
        return "too late"
    })
    fmt.Println("Timeout:", result.Err != nil)
}

FAQ

{{< faq question="Can I recover a panic in a different goroutine?" >}} No. recover only works in the same goroutine as the panic. A panic in one goroutine always crashes the program unless recovered in that goroutine. {{< /faq >}}

{{< faq question="What happens if I don't recover a panic?" >}} The program prints a stack trace and exits with a non-zero exit code. All running goroutines are terminated immediately. {{< /faq >}}

{{< faq question="Is defer expensive?" >}} defer has a small overhead (allocating the deferred function). For most code it's negligible. In hot paths, consider avoiding defer, but prioritize correctness over micro-optimization. {{< /faq >}}

{{< faq question="Can I pass arguments to a deferred function?" >} Yes. Arguments are evaluated immediately: defer fmt.Println(x) captures x's current value. For the value at return time, use a closure: defer func() { fmt.Println(x) }(). {{< /faq >}}

{{< faq question="What is the Must pattern?" >}} Functions named Must* panic on error instead of returning it. Used in initialization where failure should crash the program: regexp.MustCompile, template.Must. {{< /faq >}}

Try It Yourself

package main

import (
    "fmt"
    "time"
)

func main() {
    // defer with recover example
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Recovered:", r)
        }
        fmt.Println("Cleanup complete")
    }()

    fmt.Println("Starting work")

    // Simulated processing
    for i := 0; i < 5; i++ {
        fmt.Printf("Processing %d...\n", i)
        time.Sleep(10 * time.Millisecond)

        if i == 2 {
            fmt.Println("Something went wrong!")
            panic(fmt.Sprintf("error at step %d", i))
        }
    }

    fmt.Println("Work complete") // Never reached
}

Expected output:

Starting work
Processing 0...
Processing 1...
Processing 2...
Something went wrong!
Recovered: error at step 2
Cleanup complete

What's Next

Now that you understand panic and recover, learn about packages, modules, and import management in Go.

Topic Description Link
Go Packages and Modules go.mod, imports, visibility {{< ref "15-packages" >}}
Go Testing Testing, benchmarks, coverage {{< ref "16-testing" >}}
Rust panic! Compare Rust's panic! macro and Result Rust

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro

Home Browse Go