Skip to content
AgileLogics
6 min read

defer, panic & recover

Prerequisites: Error handling in depth

  • defer schedules a function call to run when the surrounding function returns — whether it returns normally or because of a panic. It is the standard way to release resources: defer f.Close(), defer mu.Unlock().
  • panic stops normal execution for truly unexpected situations (programmer errors, impossible states). The runtime also panics on things like an out-of-range index or integer division by zero.
  • recover, called inside a deferred function, stops a panic in progress and returns the value that was passed to panic.

Ordinary failures (missing file, bad input) should be returned as error values, not panics.

Syntax

defer fmt.Println("runs last")

panic("something impossible happened")

defer func() {
    if r := recover(); r != nil {
        fmt.Println("recovered:", r)
    }
}()

Example

package main

import "fmt"

func deferOrder() {
    for i := range 3 {
        defer fmt.Println("deferred", i)
    }
    fmt.Println("loop done")
}

func argsEvaluatedEarly() {
    x := 1
    defer fmt.Println("deferred x =", x) // x is evaluated now, not later
    x = 2
    fmt.Println("x =", x)
}

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

func main() {
    deferOrder()
    argsEvaluatedEarly()

    fmt.Println(safeDivide(10, 2))
    fmt.Println(safeDivide(1, 0))
    fmt.Println("program still running")
}

Expected output

loop done
deferred 2
deferred 1
deferred 0
x = 2
deferred x = 1
5 <nil>
0 recovered: runtime error: integer divide by zero
program still running

Three rules explain this output:

  1. Deferred calls run in last-in, first-out order.
  2. A deferred call's arguments are evaluated immediately; only the call itself is delayed.
  3. A deferred function can read and change named result values — that is how safeDivide sets err after recovering.

Common mistakes

  • Using defer inside a long loop, such as defer f.Close() for each of thousands of files. Nothing is closed until the function returns. Move the loop body into its own function.
  • Calling recover() outside a deferred function, or in a function called by the deferred function — it returns nil and doesn't stop the panic.
  • Expecting recover to catch a panic in a different goroutine. Each goroutine must recover its own panics; an unrecovered panic in any goroutine crashes the whole program.
  • Using panic/recover as a replacement for exceptions in normal control flow. Return errors instead.

Practice

Write mustPositive(n int) int that panics with the message "negative number" when n < 0 and otherwise returns n. Write try(n int) that calls it, recovers from any panic, and prints either the value or recovered: <message>. Call try(5) and try(-1).

Show solution
package main

import "fmt"

func mustPositive(n int) int {
    if n < 0 {
        panic("negative number")
    }
    return n
}

func try(n int) {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("recovered:", r)
        }
    }()
    fmt.Println("value:", mustPositive(n))
}

func main() {
    try(5)  // value: 5
    try(-1) // recovered: negative number
}