# Go Defer Panic Recover

> Guides Go's defer, panic, and recover — the LIFO ordering and arguments-evaluated-at-the-defer-statement semantics, why defer is near-free since open-coded defers (so don't avoid it except in tight loops), the loop-defer handle-leak pitfall, panic only for programmer bugs and unrecoverable states (not ordinary failure), and recover only inside a deferred function at a boundary to convert a panic into an error. Auto-invokes when writing or editing defer, panic, recover, or deferred cleanup, and on "why does this defer run in the wrong order", "should this panic", or "how do I stop a panic from crashing the process". Routes ordinary error-as-value handling to go-error-handling.

- Skill: `ctoth/go-defer-panic-recover` (Agent Skill, multi-file: 3 files)
- Install (CLI): `npx skillmds@latest add ctoth/go-defer-panic-recover`
- Raw SKILL.md: https://api.skillmd.com/api/skills/ctoth/go-defer-panic-recover/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: ctoth (https://skillmd.com/u/ctoth)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/ctoth/go-defer-panic-recover

---


# Go Defer, Panic, and Recover

> "Deferred function calls are executed in Last In First Out order after the surrounding function returns." · "A deferred function's arguments are evaluated when the defer statement is evaluated."
> — [Defer, Panic, and Recover (Go blog)](https://go.dev/blog/defer-panic-and-recover)

> "Don't use panic for normal error handling. Use error and multiple return values."
> — [Go Code Review Comments — Don't Panic](https://go.dev/wiki/CodeReviewComments#dont-panic)

`defer`, `panic`, and `recover` are three small mechanisms with sharp edges. `defer` is the everyday one — get its evaluation timing and loop behavior right. `panic` and `recover` are the rare ones — used for ordinary failure they turn Go into a worse exception language. This skill owns the **mechanics and policy**; ordinary errors-as-values route to **`go-error-handling`**.

---

## 1. What `defer` Actually Does

Four facts, each load-bearing:

1. **LIFO at return.** "Deferred function calls are executed in Last In First Out order after the surrounding function returns" ([Go blog](https://go.dev/blog/defer-panic-and-recover)). The last `defer` runs first.
2. **Arguments evaluated at the `defer` statement.** "A deferred function's arguments are evaluated when the defer statement is evaluated" ([Go blog](https://go.dev/blog/defer-panic-and-recover)) — *not* when the call later runs. This is the classic gotcha (Section 3).
3. **Runs even on panic.** "When the function F calls panic, execution of F stops, any deferred functions in F are executed normally, and then F returns to its caller" ([Go blog](https://go.dev/blog/defer-panic-and-recover)). This is why `defer` is the right place for cleanup — it fires on every return path, including a panic.
4. **Can modify named return values.** "Deferred functions may read and assign to the returning function's named return values" ([Go blog](https://go.dev/blog/defer-panic-and-recover)). The mechanism behind recover-to-error (Section 6) and `Close`-error capture (owned by `go-error-handling`).

The everyday payoff: "Deferring a call to `Close` ... guarantees you will never forget to close the file ... [and] the close sits near the open, which is much clearer than placing it at the end" ([Effective Go](https://go.dev/doc/effective_go)).

---

## 2. The Rules

| Rule | The discipline | Source |
|---|---|---|
| `defer` is for cleanup | Put release next to acquire; it runs on every return path | "an effective way to handle resource cleanup regardless of which return path is taken" ([Effective Go](https://go.dev/doc/effective_go)) |
| Args bind at the `defer` line | Don't expect a deferred arg to read a later value | "arguments are evaluated when the defer statement is evaluated" ([Go blog](https://go.dev/blog/defer-panic-and-recover)) |
| Don't `defer` in a loop for per-iteration cleanup | Close in the loop body or extract a function | Loop-defer accumulates until the function returns (Section 4) |
| `defer` is not a perf concern | Near-zero overhead since Go 1.14 open-coded defers | "improves the performance of most uses of `defer` to incur almost zero overhead" ([Go 1.14](https://go.dev/doc/go1.14)) |
| `panic` is for bugs, not failure | Ordinary failures return an `error` | "Don't use panic for normal error handling" ([CodeReviewComments](https://go.dev/wiki/CodeReviewComments#dont-panic)) |
| `recover` only in a deferred func | Anywhere else it returns nil and does nothing | "Recover is only useful inside deferred functions" ([Go blog](https://go.dev/blog/defer-panic-and-recover)) |
| Recover only at a boundary | Convert a panic to an error; re-panic what you can't handle | "regains control of a panicking goroutine" ([Go blog](https://go.dev/blog/defer-panic-and-recover)) |

---

## 3. Arguments Evaluate at the `defer` Statement

The single most common `defer` surprise: the deferred *call's arguments* are snapshotted when `defer` runs, even though the *call* runs at return.

```go
// WRONG — expecting start to be re-read at return; it is captured as the start value
func timed() {
	start := time.Now()
	defer log.Printf("took %v", time.Since(start)) // time.Since(start) runs NOW, logging ~0
	doWork()
}

// RIGHT — defer a closure so the expression runs at return time
func timed() {
	start := time.Now()
	defer func() { log.Printf("took %v", time.Since(start)) }()
	doWork()
}
```

The argument form evaluates `time.Since(start)` immediately; the closure form defers the whole expression. The same fact makes LIFO observable: `for i := 0; i < 5; i++ { defer fmt.Printf("%d ", i) }` "Prints: 4 3 2 1 0" ([Effective Go](https://go.dev/doc/effective_go)) — each `i` is captured at its iteration, then the calls run in reverse.

---

## 4. The Loop-Defer Pitfall

`defer` fires at **function** return, not at the end of the loop body. Deferring `Close` inside a `for` accumulates open handles for the whole function — a file-descriptor leak that only shows up under load.

```go
// WRONG — every file stays open until processAll returns; thousands of names => fd exhaustion
func processAll(names []string) error {
	for _, name := range names {
		f, err := os.Open(name)
		if err != nil {
			return err
		}
		defer f.Close() // does NOT close at end of iteration
		use(f)
	}
	return nil
}

// RIGHT — extract the body so defer fires per item
func processAll(names []string) error {
	for _, name := range names {
		if err := processOne(name); err != nil {
			return err
		}
	}
	return nil
}

func processOne(name string) error {
	f, err := os.Open(name)
	if err != nil {
		return err
	}
	defer f.Close() // fires at processOne's return — once per item
	use(f)
	return nil
}
```

Note the inverse: in a non-loop function, **don't** avoid `defer` for performance. Open-coded defers (Go 1.14) made it "incur almost zero overhead" ([Go 1.14](https://go.dev/doc/go1.14)); Uber's guide agrees it "has an extremely small overhead and should be avoided only if you can prove that your function execution time is in the order of nanoseconds" ([Uber Go Style Guide](https://github.com/uber-go/guide/blob/master/style.md#defer-to-clean-up)).

---

## 5. `panic` Is for Programmer Errors, Not Ordinary Failure

`panic` unwinds the stack and, unrecovered, crashes the process. Reserve it for bugs and truly unrecoverable states: a violated invariant, an impossible `switch` case, a nil that the code's own contract forbids. Ordinary, foreseeable failure — bad input, a missing file, a failed lookup — is an `error` the caller decides about. "Don't use panic for normal error handling. Use error and multiple return values" ([CodeReviewComments](https://go.dev/wiki/CodeReviewComments#dont-panic)); the Google style guide repeats it: "Do not use `panic` for normal error handling. Instead, use `error` and multiple return values" ([Google Go Style Guide](https://google.github.io/styleguide/go/decisions#dont-panic)).

Legitimate uses are narrow: an `init` that cannot establish a required invariant — "`panic` is reasonable during initialization if the library cannot set itself up" ([Effective Go](https://go.dev/doc/effective_go)) — or a `MustCompile`-style helper for compile-time-constant input. The depth of returning errors instead (wrapping, `errors.Is`/`As`, named-return decoration) is owned by **`go-error-handling`**.

---

## 6. `recover` Only in a Deferred Func, Only at a Boundary

"Recover is a built-in function that regains control of a panicking goroutine. Recover is only useful inside deferred functions. During normal execution, a call to recover will return nil and have no other effect" ([Go blog](https://go.dev/blog/defer-panic-and-recover)). So `recover` does something only when (a) it sits in a deferred function and (b) a panic is in flight. Use it at a **boundary** — the top of a request handler, or a goroutine you own — to convert a panic into an error rather than crash, then re-establish invariants.

```go
// RIGHT — a boundary converts a panic into an error via a named return
func safeInvoke(fn func()) (err error) {
	defer func() {
		if r := recover(); r != nil {
			err = fmt.Errorf("recovered: %v", r) // re-establish: a panic is now an error
		}
	}()
	fn()
	return nil
}

// WRONG — recover outside a deferred func: returns nil, catches nothing, fn's panic still crashes
func brokenGuard(fn func()) {
	if r := recover(); r != nil { // never true here
		log.Println(r)
	}
	fn()
}

// WRONG — swallowing the panic with no log and no re-raise: the bug vanishes silently
func swallow(fn func()) {
	defer func() { recover() }()
	fn()
}
```

Two non-negotiables:

- **Re-panic what you can't handle.** A `recover` that catches every value and discards it hides real bugs. Inspect the recovered value; if it is not one you can turn into a clean error, log it and `panic(r)` again.
- **Don't recover across goroutines.** A deferred `recover` only catches a panic in *its own* goroutine. "if `do(work)` panics, the error is logged and the goroutine exits cleanly without killing other goroutines" ([Effective Go](https://go.dev/doc/effective_go)) — but a panic in a goroutine you spawned without its own `recover` crashes the whole program. Wrap each goroutine you own. See **`go-concurrency-goroutines`**.

`recover` is not general control flow. Don't use panic/recover as exceptions to jump across ordinary call layers — that is the exception-language anti-pattern `go-idiomatic-discipline` bans.

---

## 7. Who Suffers When This Is Done Badly

The author never feels it at write time; someone downstream does:

- The **on-call engineer** paged when a service exhausts file descriptors under load — a `defer f.Close()` inside a `for` that never closed until the long-lived function returned (Section 4).
- The **whole user base** taken down when one request, or one un-wrapped worker goroutine, panics on bad input the code should have returned as an error (Sections 5–6).
- The **next debugger** who spends an afternoon chasing a bug that a bare `defer func(){ recover() }()` silently swallowed three layers down — no log, no re-raise, no trace (Section 6).

`defer`/`panic`/`recover` done well is invisible; done badly it is a 3am page or a corrupted-state mystery.

---

## 8. Routing to the Specific Skills

This skill owns `defer`/`panic`/`recover` mechanics and policy. Adjacent depth lives elsewhere:

- **`go-idiomatic-discipline`** — the policy root: "don't panic for ordinary failure" is one of its headline floor rules; panic-as-exceptions is one of its named anti-patterns.
- **`go-error-handling`** — errors as values: `%w` wrapping, `errors.Is`/`As`, and the `defer`-based named-return *error decoration* and `Close`-error-capture idioms (this skill teaches the `defer`/named-return *mechanism*; that skill teaches the error idiom built on it). Return an error instead of panicking — owned there.
- **`go-concurrency-goroutines`** — a panic in a goroutine you spawned without its own deferred `recover` crashes the process; wrap every goroutine you own.
- **`go-context`** — cancellation and `defer cancel()`; a `recover` boundary often sits at the same request edge where a context is derived.

---

## 9. Reference Files

High-frequency `defer`/`panic`/`recover` anti-patterns in LLM-generated Go, each with wrong/right code and citations:

[references/common-mistakes.md](references/common-mistakes.md)

Source provenance for every claim in this skill:

[references/sources.yaml](references/sources.yaml)

