Recover

Learn how the built-in recover function works to intercept panics, its use inside deferred functions, and the pattern for converting panics into errors in Go

What recover Does

recover is a built-in function that stops a panicking goroutine in its tracks and returns the value that was passed to panic. It is the only way to regain control after a panic without letting the program crash.

The signature is simple:

func recover() interface{}

When called during normal execution — that is, when no panic is in progress — recover returns nil and does nothing else. It only has an effect inside a deferred function that runs while the goroutine is unwinding from a panic. If you call recover anywhere else, even inside a function that is not directly deferred, it silently returns nil.

recover outside a deferred function does nothing:

Placing recover() in the main body of a function, or in a helper called by a deferred function (but not deferred itself), will always return nil. The call must be in a function that was scheduled with defer and is currently executing because of a panic.

This restriction exists because the Go runtime needs a clear, single point where the panic can be intercepted. The deferred function that calls recover acts as a barrier: once it runs, the panic sequence stops, and the program continues normally from the point right after the function that panicked.

How recover Works with defer and panic

To understand recover, you need to picture what happens when panic is called.

When a function calls panic, normal execution of that function stops immediately. The runtime then starts running all the deferred functions in that function, in LIFO order. After the last deferred function finishes, control returns to the caller — but the caller does not resume normally. Instead, it behaves as if it also called panic. This process repeats, unwinding the call stack goroutine by goroutine, until every function in the goroutine has returned, at which point the program exits with a stack trace.

If, during this unwinding, a deferred function calls recover, the panic stops. The deferred function can inspect the value passed to panic and decide what to do. The goroutine does not terminate. Execution resumes after the function that contained the recovering deferred call — specifically, execution continues normally after the call to the function that panicked. All other deferred functions that were supposed to run after the recovering one still execute, because the panic sequence is terminated at that point.

Put plainly: recover turns a catastrophic unwinding back into ordinary sequential code flow.

A minimal working example

package main
import "fmt"
func main() {
    safeFunction()
    fmt.Println("Program continued normally.")
}
func safeFunction() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Recovered from:", r)
        }
    }()
    panic("something went wrong")
    fmt.Println("This line never runs")
}

When you run this, you see:

Recovered from: something went wrong
Program continued normally.

The deferred function fires during the panic. recover() captures the string "something went wrong". Because the deferred function returns without re-panicking, the panic is fully resolved. The line after the panic call does not execute, but safeFunction returns to main, which then prints its message.

Without the deferred function and recover, the program would crash with a panic: something went wrong message and a stack trace.

The Standard recover Pattern

Idiomatic Go code uses recover in a very specific shape. You almost always see it directly inside a deferred function literal, combined with a named return value to convert the panic into an ordinary error.

1

Step 1: Use a named error return value

The function that may panic returns a named error result. This lets the deferred function modify the return value.

func doWork() (err error) {
    // ...
}
2

Step 2: Defer a function literal that calls recover

Inside the deferred function, call recover(). If it returns non‑nil, the function can set the named error return.

defer func() {
    if r := recover(); r != nil {
        err = fmt.Errorf("internal panic: %v", r)
    }
}()
3

Step 3: Write the code that might panic

Any call to panic in the function body (or in any code called from it) will now be caught and turned into an error.

// some operation that might panic
risk := someCondition()
if risk {
    panic("unexpected condition")
}
4

Step 4: Return normally

After the deferred function runs and sets err, the function returns to its caller with the error. The caller never sees a crash.

return nil // err is set by the deferred function if a panic occurred

This pattern is the backbone of several standard library packages. It allows a library to use panic internally to escape from deeply nested error conditions, but expose a clean error‑returning API to the caller.

A correctly structured recovery:

If your code follows this pattern and you see the function returning an error like internal panic: ..., you know the recovery mechanism is working as intended.

Common Mistakes with recover

These mistakes lead to panics that escape, incorrect behavior, or code that silently swallows panics without handling them properly.

Calling recover outside a deferred function

func broken() {
    // This has no effect — no panic is in progress during normal execution.
    if r := recover(); r != nil {
        fmt.Println("Never printed")
    }
    panic("boom")
}

recover only captures a panic if the call happens inside a deferred function while the stack is unwinding. In regular code, it returns nil.

Calling recover in a helper that is not itself deferred

func handler() {
    defer func() {
        // This deferred function does not directly call recover.
        // Instead, it calls a helper. That helper is not deferred.
        helper()
    }()
    panic("oh no")
}
func helper() {
    if r := recover(); r != nil {
        fmt.Println("Recovered:", r) // unreachable
    }
}

The helper runs because it was invoked by the deferred function, but recover needs to be called directly from the deferred function — the function that was pushed onto the defer stack. In this example, the deferred function is func() { helper() }, so the call to recover inside helper is not inside a deferred function and returns nil.

Wrap recover directly in the deferred function:

To catch a panic, place recover() immediately inside the body of the function you pass to defer. Do not hand it off to another function unless that function is also deferred.

Ignoring the return value of recover

A call to recover that does not inspect the returned value is useless. If you write defer recover() without capturing the result, the panic is stopped but you have no information about what went wrong, and no error is returned to the caller. The program silently swallows a severe problem, which can mask serious bugs.

Trying to recover from a panic in a different goroutine

Each goroutine has its own independent panic/recover stack. A deferred function in one goroutine cannot recover a panic that happened in another.

func main() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Recovered:", r) // never called
        }
    }()
    go func() {
        panic("goroutine failure")
    }()
    time.Sleep(time.Second)
}

The main goroutine’s deferred function never sees the panic from the spawned goroutine. The spawned goroutine crashes, and if it has no recovery of its own, the entire program terminates with a panic message from that goroutine.

Cross-goroutine recovery does not work:

If you launch a goroutine that might panic, it must contain its own deferred recovery logic. The parent goroutine cannot catch its panics.

Converting Panics into Errors

The most common legitimate use of recover is to turn a panic into a standard error that follows Go’s explicit error‑handling model. This is how packages like encoding/json protect their callers from internal panics.

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

If b is zero, the integer division causes a runtime panic (a runtime error: integer divide by zero). The deferred function catches it, sets err, and the function returns an error instead of crashing. The caller can check if err != nil and handle it like any other error.

This approach is useful when a function relies on operations that can panic — for instance, type assertions on interface values from external input — and you want to protect the program while still returning a meaningful error.

The same pattern, but with a generic helper, is common:

func withRecover(fn func()) (err error) {
    defer func() {
        if r := recover(); r != nil {
            err = fmt.Errorf("recovered: %v", r)
        }
    }()
    fn()
    return nil
}

Here, withRecover runs an arbitrary function and converts any panic into an error. Calling withRecover(func() { panic("bad") }) returns an error, not a crash.

Recover in Production — The encoding/json Example

A real-world demonstration of recover lives inside the Go standard library’s encoding/json package. When encoding or decoding deeply nested JSON, the code often descends through many recursive function calls. If an error is detected deep in the recursion, returning it explicitly would require threading error checks back up through every level of the call stack — adding complexity and performance overhead.

Instead, encoding/json uses panic internally as a non‑local exit mechanism. When an error occurs, the deep function calls panic(err). At the top level, a deferred function calls recover and transforms the panic into a normal error return that the package’s public API surfaces.

A simplified sketch looks like this:

func marshal(v interface{}) (out []byte, err error) {
    defer func() {
        if r := recover(); r != nil {
            // Convert internal panic back into an error for the caller
            err = fmt.Errorf("json: %v", r)
        }
    }()
    // recursive traversal code that might panic on error
    encodeValue(reflect.ValueOf(v))
    return out, nil
}

The caller of marshal never sees a panic. It receives a clean error value, preserving Go’s explicit error‑handling conventions while allowing the internal implementation to stay straightforward.

This technique is deliberate, not accidental. It is only appropriate when a package uses panic as a controlled escape and recovers at a well‑defined boundary. Libraries that expose a panic to the caller without recovery are considered buggy.

Recover and Goroutines

A recovery point is local to a single goroutine. That means every goroutine that can panic needs its own deferred recovery logic. If you are writing a long‑running background goroutine — say, a worker processing tasks — you must ensure that a panic in one task does not take down the entire program.

A typical worker loop includes a recovery block:

func worker(tasks <-chan Task) {
    for task := range tasks {
        func() {
            defer func() {
                if r := recover(); r != nil {
                    log.Printf("task panicked: %v", r)
                }
            }()
            process(task)
        }()
    }
}

Each task runs inside an anonymous function that defers recovery. If process(task) panics, the deferred function logs it, and the worker moves on to the next task. Without that recovery, a single panicking task would kill the entire worker goroutine — and if that goroutine were the only one consuming the channel, the program would stall or crash.

Libraries that start their own goroutines are responsible for handling panics inside them. If they fail to do so, the caller has no way to recover.

Performance Considerations

In normal execution, when no panic occurs, the overhead of setting up a deferred recovery is minimal. The compiler and runtime have been optimized to make defer cheap, especially in Go 1.14 and later.

When a panic actually happens, the cost is significant. The runtime must unwind the stack, execute deferred functions, and potentially halt the goroutine. This is not a path you want to take on every error; that’s why idiomatic Go uses panic only for truly exceptional conditions and handles ordinary errors through return values.

recover itself adds no noticeable cost. The expense is entirely in triggering and resolving the panic. So the pattern of deferring a recovery guard around a code block is safe even in performance‑sensitive code, as long as panics remain rare.

recover is not a general control flow mechanism:

Using panic/recover as a substitute for if err != nil will make programs slower and harder to reason about. Reserve it for the few cases where returning errors through every layer of a deep call stack would be unreasonably convoluted.

Summary

recover is a small function with a sharply defined purpose. It exists to catch panics that escape from the normal error‑returning discipline, and it only works when called directly inside a deferred function. The right way to use it is almost always the pattern of converting a panic into a normal error via a named return value — as demonstrated by the standard library’s encoding/json package.

The most common mistakes are calling recover outside a deferred function, hiding it behind a helper, ignoring its return value, and forgetting that it cannot cross goroutine boundaries. Knowing these pitfalls makes the difference between a safety net and a silent bug.

When you write a function that uses recover, you are making a promise to the caller: whatever happens internally — even a runtime panic — will be presented as a clean error. This pattern keeps Go’s error handling explicit and predictable, even when the underlying code needs to break out of deeply nested logic.