---
title: 'Panic and Recovery'
source: 'https://academia.sh/en/courses/go-type-system/panic-and-recover'
course: 'Interfaces, Errors and Generics'
language: en
updated: '2026-09-14T10:59:34+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Compares the visible channel the error-returning and panicking forms of the same task leave in the signature'
    - 'Shows by running it that recover only works in the deferred function that calls it directly'
    - 'Proves by measurement that when a recovered panic is not converted into an error, the information is lost entirely'
---

# Panic and Recovery

A function can panic instead of returning an error, and this choice leaves no trace in the signature: of two functions, one returning (int, error) and the other returning only int, one always produces a value while the other can crash at run time. recover only works in the deferred function that calls it directly; if a recovered panic is not converted into an error, the information is lost entirely. No stack trace is printed; only panic=true/false and the recovered value's type are measured.

The previous three lessons measured that a function always reports its failure with the same
contract: the last return value is an `error`. No matter how long the chain grew, no matter how
much context each layer added, the signature always stayed the same. This lesson steps outside
that contract: in Go, there is also a way for a function to fail that never appears in the
signature at all: panic. This lesson's question is the topic's question in its sharpest form: if
panic's binding is never established at all, how can the caller know this in advance?

Panic is a mechanism the language's own run time also uses: reaching outside a slice's bounds,
reading from an uninitialized pointer before writing to a map, dividing by zero — all of these
are panics produced by the run time itself. This lesson measures only the `panic` calls a
programmer writes directly, but the observation is the same for both: neither appears in the
signature.

## The Channel Visible in the Signature, the Exit That Is Not

Writing the same task in two forms is possible: a `Store` type's get function could either
return `(int, error)` and carry the not-found state in the second value, or return only `int`
and panic when the item is not found. The measurement below builds both forms in a three-layer
call chain — `inner`, `middle` calling it, `outer` calling that. Three layers were chosen
deliberately: the same depth as the chain in the Error Wrapping and Unwrapping lesson, but this
time the layers add no context at all, they only forward the call — what is measured is not how
context is carried, but how visible the failure channel itself stays across the layers.

```go
// binding.go — is panic's binding established in a declaration, inside the value, or nowhere at all
package main

import "fmt"

var declaration, value, none int

func observe(name, location, evidence string) {
	switch location {
	case "declaration":
		declaration++
	case "value":
		value++
	default:
		none++
	}
	fmt.Printf("%-54s %-12s %s\n", name, location, evidence)
}

func summary() {
	fmt.Printf("binding: %d declaration, %d value, %d nowhere\n", declaration, value, none)
}
```

```go
// forms.go — the same task in two forms: error-returning and panicking, a three-layer chain
package main

import "fmt"

type Store struct{ items map[string]int }

func (s Store) innerError(name string) (int, error) {
	v, ok := s.items[name]
	if !ok {
		return 0, fmt.Errorf("not found: %s", name)
	}
	return v, nil
}
func (s Store) middleError(name string) (int, error) { return s.innerError(name) }
func (s Store) outerError(name string) (int, error)  { return s.middleError(name) }

func (s Store) innerPanic(name string) int {
	v, ok := s.items[name]
	if !ok {
		panic(fmt.Sprintf("not found: %s", name))
	}
	return v
}
func (s Store) middlePanic(name string) int { return s.innerPanic(name) }
func (s Store) outerPanic(name string) int  { return s.middlePanic(name) }

func panics(f func()) (p bool) {
	defer func() {
		if recover() != nil {
			p = true
		}
	}()
	f()
	return false
}

func main() {
	store := Store{items: map[string]int{"a": 1}}

	result, err := store.outerError("missing")
	observe("the error-returning signature's visible channel", "declaration", "(int, error) -- the second value carries the error")
	observe("the error-returning call passing through three layers", "value", fmt.Sprint(result, " ", err))

	panicked := panics(func() { store.outerPanic("missing") })
	observe("the panicking signature's visible channel", "none", "int -- there is no second channel")
	observe("the panicking call passing through three layers", "none", fmt.Sprint("panic=", panicked))

	summary()
}
```

```
the error-returning signature's visible channel        declaration  (int, error) -- the second value carries the error
the error-returning call passing through three layers  value        0 not found: missing
the panicking signature's visible channel              none         int -- there is no second channel
the panicking call passing through three layers        none         panic=true
binding: 1 declaration, 1 value, 2 nowhere
```

**EH18.** `innerError`'s signature is `(int, error)` — the failure channel stands directly in
the **declaration**; `middle` and `outer` carry this second value unchanged. `innerPanic`'s
signature, on the other hand, is only `int`; there is no second slot for failure. The `panics`
tool (like `compiles`, borrowed from the Functions lesson, from K01's toolkit) here observes not
merely whether the call passed, but whether it panicked, and it never prints a stack trace. The
call passing through the third layer really has panicked; but **seeing** this required writing a
wrapper like `panics` — an ordinary call (`store.outerPanic("missing")`) crashes without giving
any sign.

**EH19.** These two lines build the lesson's central observation: in the error-returning form,
the failure channel occupies **a place in the signature** (declaration); in the panicking form,
it occupies **no place at all**. This is a fourth case that falls outside the topic's
declaration / use site / value triad — the binding is neither written in the source, nor
something the compiler checks at an assignment point, nor a pair the value carries. Someone
calling `innerPanic` cannot learn from the signature that this function might fail; the only way
to learn this is either documentation (a text outside the source) or reading the function's
body.

This asymmetry has a cost, but the cost runs in both directions. In the error-returning form,
every layer (`inner`, `middle`, `outer`) has to carry the second value by hand — writing
`return ..., err` three times across three lines, or at least noticing the second value three
times. In the panicking form, the layers write nothing at all; an ordinary function call is
enough, because the panic propagates on its own. The cost of visibility is a repeated piece of
writing at every layer; the cost of invisibility is that the caller is left with no warning at
all.

## Recovery Only Works in the Deferred Call

`recover` is the only way to stop a panic and return to normal execution — but it only works in
a single position: **directly inside** a function deferred with `defer`. When the `recover` call
is moved into a helper function, `recover` no longer catches anything, even though that helper
is called by the deferred function.

This restriction is not arbitrary: `recover` itself asks the run time which frame of the call
stack it is being called from, and only returns something if that frame is **the frame the
panicking function directly deferred**. This explains why burying `recover` inside an ordinary
helper function to make it reusable — a "write once, use everywhere" shortcut — mostly does not
work.

```go
// recover.go — recover only works inside the defer that calls it directly
package main

import "fmt"

func panics(f func()) (p bool) {
	defer func() {
		if recover() != nil {
			p = true
		}
	}()
	f()
	return false
}

func directRecover() (recovered bool) {
	defer func() {
		if recover() != nil {
			recovered = true
		}
	}()
	panic("test")
}

func indirectRecoverHelper() interface{} {
	return recover()
}

func indirectRecover() (recovered bool) {
	defer func() {
		r := indirectRecoverHelper()
		if r != nil {
			recovered = true
		}
	}()
	panic("test")
}

func main() {
	a := directRecover()
	observe("recover called directly inside defer", "value", fmt.Sprint(a))

	escaped := panics(func() { indirectRecover() })
	observe("recover called through a helper, did it escape outward", "value", fmt.Sprint(escaped))
	summary()
}
```

```
recover called directly inside defer                   value        true
recover called through a helper, did it escape outward value        true
binding: 0 declaration, 2 value, 0 nowhere
```

**EH20.** `directRecover` calls `recover()` directly in the body of
`defer func() { ... }()` and catches the panic — `recovered` comes back `true`.
`indirectRecover`, on the other hand, looks as though it is trying to do the same thing: it has
written a `defer`, that `defer` calls a function, and that function calls `recover()`. But
because there is a call layer in between, `recover` sees that the function calling it is **not**
deferred and returns `nil` — the panic is not caught, `indirectRecover` escapes outward on its
own. Seeing this escape required calling it wrapped in a `panics` wrapper; the second line shows
that this outer wrapper catches the panic.

**EH21.** This shows that `recover` is one of the most narrowly defined places in the language:
it only looks at the question "is the function currently running the function the panicking call
directly deferred." Call depth does not matter — `directRecover` itself could also have been
called inside `panics`; the result would not change; what matters is that the function
**calling** `recover` is itself deferred with `defer`. This rule explains why attempts to make
`recover` reusable by moving it into a helper function fail.

## Limiting Measure: Recovery Does Not Destroy the Panic, It Carries It

When a panic is recovered, the information it carries (the value given to panic) is not
lost — `recover()` returns it. But **using** this information is the caller's responsibility.
The two functions below catch the same panic; one converts it into an `error`, the other
discards it silently.

These two functions also show the bridge between panic and error: once a panic is recovered, it
is entirely possible to put the result back into Go's ordinary error contract — the
`(result, error)` form. `runRecovering` shows a pattern commonly used to hide a subsystem that
panics behind a library's outer surface and present its caller with an ordinary `error`.

```go
// recovery.go — recovery does not destroy the panic, it carries it; if not converted to an error, the information is lost
package main

import (
	"fmt"
	"strings"
)

func vowelCount(word string) int {
	if word == "" {
		panic("empty word")
	}
	count := 0
	for _, r := range word {
		if strings.ContainsRune("aeiouAEIOU", r) {
			count++
		}
	}
	return count
}

func runRecovering(f func() int) (result int, err error) {
	defer func() {
		if r := recover(); r != nil {
			err = fmt.Errorf("recovered panic (%T): %v", r, r)
		}
	}()
	return f(), nil
}

func runSilently(f func() int) (result int) {
	defer func() {
		recover()
	}()
	return f()
}

func main() {
	result1, err1 := runRecovering(func() int { return vowelCount("") })
	observe("the empty word's recovered panic converted to an error", "value", fmt.Sprint(result1, " ", err1))

	emptyResult := runSilently(func() int { return vowelCount("") })
	realZeroResult := runSilently(func() int { return vowelCount("krt") })
	observe("the empty word's recovered result", "none", fmt.Sprint(emptyResult))
	observe("the vowel-less real word's result", "none", fmt.Sprint(realZeroResult))

	summary()
}
```

```
the empty word's recovered panic converted to an error value        0 recovered panic (string): empty word
the empty word's recovered result                      none         0
the vowel-less real word's result                      none         0
binding: 0 declaration, 1 value, 2 nowhere
```

**EH22.** `runRecovering` converts the value it recovers, together with its type via `%T`, into
an `error` — the value `vowelCount("")` gives when it panics is a string (`string`), and this
appears in the returned error. Recovery does not **destroy** the panic; it carries the
information it holds into another form, into a channel (`error`) the signature already has.
This line proves that even once a panic is recovered, `%T` can still show which value was
panicked with.

**EH23.** `runSilently`, on the other hand, catches the same panic but writes the value
`recover()` returns nowhere. The result: the `0` returned when `vowelCount("")` panics and is
recovered, and the `0` returned by `vowelCount("krt")` (a genuinely computed string containing no
vowels), are **indistinguishable** from each other. This is the lesson's limiting measure:
recovery carries the information, but if the recovered value is not written anywhere, that
carried information is lost entirely, somewhere the caller cannot reach — what remains looks
like a zero that "seems successful." In this example, even the zero itself is a trap:
`vowelCount("krt")` is a genuinely computed, legitimate zero; there is no signal difference
between it and a panic-derived zero. In general, a return value left silently after recovery can
**always** be confused with its type's zero value — a risk that holds no matter which type is
chosen.

These three measurements together give a definite answer to the topic's question: panic's
binding is not in the declaration (there is no trace in the signature), not in the use site
(there is no assignment point the compiler looks at), and not in the value either (the caller
cannot read the possibility of panicking from any type) — panic is established in **none** of
the topic's three places. Its only establishment point is the run time, which never appears at
all until the recovery code runs; and even recovery compensates for nothing unless the
information is carried by hand into another channel.

## Summary

- An error-returning function's signature sets aside a channel for failure (`declaration`); a
  panicking function's signature has no such channel at all — panic is established in none of
  the topic's three places.
- `recover` only works inside the deferred (`defer`) function that calls it directly; once moved
  into a helper function, it returns `nil` and the panic keeps propagating uncaught.
- When a panic is recovered, the value it carries is not lost; it can be read together with its
  type via `%T` and converted into an `error`.
- If the recovered value is not written anywhere, the information is lost entirely: a recovered
  panic's zero result becomes indistinguishable from a genuine zero result.
- No measurement prints a stack trace, a memory address or a goroutine number; only
  `panic=true`/`panic=false` and the recovered value's type read with `%T` are recorded.

## Next Step

This topic asked a single question four times: how does a type satisfy the `error` interface,
where does an error's identity stand, how is a chain's context preserved, and finally, what
happens when an exit's binding is never established at all. The first three lessons all found
the same answer: the binding is established **at the use site** (an assignment to a variable, a
method-set check) or carried **inside the value** (the dynamic type, identity, the wrapped
chain). This lesson measured a fourth case that falls outside both of these: panic's binding is
established nowhere at all and never appears in the signature — the caller cannot learn from a
function's signature that it might panic.

The next topic, Generics, does exactly the opposite of this. A generic function's type parameter
is also never written in the source — the caller can call a function without specifying any type
at all. But unlike panic, this binding is established by the **compiler**, and exactly at the
call site: every call determines, at compile time, which type it is instantiated with. The same
"not written in the source" observation reaches two exactly opposite conclusions in the two
topics — panic escapes every check, generics carries every check to the use site, and the
binding never enters a value.
