panic, recover and defer
When to panic and how to recover gracefully
panic, recover and defer is a free Go Academy lesson on CoddyKit — lesson 4 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the Go Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.
What Is panic?
panic stops normal execution of the current goroutine, unwinds the call stack running deferred functions, and crashes the program if not recovered:
package main
import "fmt"
func mustPositive(n int) int {
if n <= 0 {
panic(fmt.Sprintf("mustPositive: got %d", n))
}
return n
}
func main() {
fmt.Println(mustPositive(5)) // 5
// fmt.Println(mustPositive(-1)) // panics!
}When to Use panic
Use panic only for:
- Truly unrecoverable programmer errors (broken invariants)
- Impossible conditions that indicate a bug
- Startup failures (missing config, failed connections during init)
Never use panic for expected failure conditions — use error instead.
defer Basics
defer schedules a function call to run when the surrounding function returns, regardless of how it returns (normal, error, panic):
package main
import "fmt"
func main() {
defer fmt.Println("third")
defer fmt.Println("second") // LIFO order
defer fmt.Println("first")
fmt.Println("running...")
// Output:
// running...
// first
// second
// third
}defer for Cleanup
The most common use of defer is ensuring resources are cleaned up:
package main
import ("fmt"; "os")
func readFile(path string) (string, error) {
f, err := os.Open(path)
if err != nil {
return "", fmt.Errorf("open: %w", err)
}
defer f.Close() // always runs, even if we return early
buf := make([]byte, 1024)
n, _ := f.Read(buf)
return string(buf[:n]), nil
}recover — Catching panics
recover stops a panic and returns the panic value. It must be called directly inside a defer function:
package main
import "fmt"
func safeDiv(a, b int) (result int, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("recovered from panic: %v", r)
}
}()
return a / b, nil // panics if b==0
}
func main() {
r, err := safeDiv(10, 0)
fmt.Println(r, err)
// 0 recovered from panic: runtime error: integer divide by zero
}recover Must Be in Deferred Function
recover only works if called directly inside a deferred function. Calling it elsewhere returns nil:
package main
import "fmt"
func wrong() {
// recover() here does nothing — not in defer
}
func correct() {
defer func() {
if r := recover(); r != nil {
fmt.Println("caught:", r)
}
}()
panic("oops")
}
func main() { correct() }Panic in Library Code
Library code should almost never let panics escape to callers. Convert panics to errors at the boundary:
package main
import ("fmt"; "runtime/debug")
func safeCall(fn func()) (err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("panic: %v\n%s", r, debug.Stack())
}
}()
fn()
return nil
}
func main() {
err := safeCall(func() { panic("boom") })
fmt.Println(err != nil) // true
}defer LIFO Order and Loop Gotcha
Deferred calls run in last-in, first-out (LIFO) order. Be careful deferring inside loops — use a closure or helper function:
package main
import "fmt"
func processFiles(names []string) {
for _, name := range names {
name := name // capture loop variable
defer func() { fmt.Println("close:", name) }()
}
}
func main() {
processFiles([]string{"a.txt", "b.txt", "c.txt"})
// Output (LIFO): close: c.txt, close: b.txt, close: a.txt
}defer with Named Return Values
Deferred functions can read and modify named return values — useful for annotating errors:
package main
import "fmt"
func doWork() (err error) {
defer func() {
if err != nil {
err = fmt.Errorf("doWork: %w", err)
}
}()
return fmt.Errorf("something failed")
}
func main() {
err := doWork()
fmt.Println(err) // doWork: something failed
}panic vs error: Decision Guide
Choosing between panic and error:
- Use error: expected failures (file not found, network timeout, invalid input)
- Use panic: programming errors (nil map access, out-of-bounds), impossible states, init failures
- Never use panic across API boundaries — callers cannot handle it
- Recover panics at goroutine top-level in servers to avoid crashes
Quick Check
Where must recover() be called to catch a panic?
Recap: panic, recover, defer
Summary:
panic: unwinds stack, runs defers, crashes if unrecovereddefer: runs on function exit (normal, error, or panic); LIFO orderrecover: stops panic, returns value; must be indefer- Use panic for programmer errors, not expected failures
- Convert panics to errors at API boundaries
Frequently asked questions
Is the “panic, recover and defer” lesson free?
Yes — the full text of “panic, recover and defer” is free to read here on the web, and the Go Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the Go Academy course, upgrade to CoddyKit PRO.
What will I learn in “panic, recover and defer”?
When to panic and how to recover gracefully You practise Go Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.
Do I need any experience to start Go Academy?
No prior experience is required. Go Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 4 of 4, so you can start here or from the beginning and move at your own pace.
How long does the “panic, recover and defer” lesson take?
Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.
Can I write and run code in this Go Academy lesson?
Yes. Every Go Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.
All lessons in this course
- The error Interface
- Creating Custom Errors
- Error Wrapping and Unwrapping
- panic, recover and defer