panic and recover

A panic stops normal execution: the current function quits, deferred calls run as the panic travels up the call stack, and if nothing stops it, the program crashes with a message and a stack trace. Panics are for bugs and impossible situations, not for ordinary errors. This lesson covers what causes panics, how they unwind, recover, and the narrow cases where using them is right.

What causes a panic

The runtime panics on programming mistakes it can't let continue:

CauseExample
Index out of ranges[10] on a 3-element slice
Nil pointer dereferencep.Name when p is nil
Writing to a nil mapvar m map[string]int; m["a"] = 1
Failed type assertionv.(int) when v holds a string
Integer division by zeroa / b with b == 0
Closing a closed channelLesson 52

Your own code can panic too, by calling the built-in panic with any value — usually an error or a string.

How a panic unwinds

When a function panics, it stops immediately. Its deferred calls run, then its caller stops and runs its deferred calls, and so on up to the top of the goroutine, where the program prints the panic value and a stack trace and exits with status 2:

GO
package main

import "fmt"

func level3() {
	defer fmt.Println("level3 deferred")
	panic("something impossible happened")
}

func level2() {
	defer fmt.Println("level2 deferred")
	level3()
	fmt.Println("never printed")
}

func main() {
	defer fmt.Println("main deferred")
	level2()
}
Output
level3 deferred
level2 deferred
main deferred
panic: something impossible happened

goroutine 1 [running]:
…
exit status 2

The full stack trace (cut short here) lists every function on the way, with file names and line numbers — usually exactly what you need to find the bug.

recover

recover stops a panic. It only works inside a deferred function: it returns the value passed to panic, and the function whose defer called it then returns normally to its caller. Outside a panic, recover returns nil:

GO
package main

import "fmt"

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() {
	fmt.Println(safeDivide(10, 2))
	fmt.Println(safeDivide(1, 0))
	fmt.Println("program continues")
}
Output
5 <nil>
0 recovered: runtime error: integer divide by zero
program continues

Setting the named result err in the deferred function turns the panic into an ordinary error for the caller.

Where recover belongs

Recovering should be rare. The legitimate places are boundaries where one failing task mustn't bring down everything else — for instance, a server handling many independent requests. Go's HTTP server does this for you: a panic in one handler is logged and that connection is closed, while the server keeps running. A job runner might do the same for each job:

GO
package main

import (
	"fmt"
	"strings"
)

func runJob(name string, job func()) {
	defer func() {
		if r := recover(); r != nil {
			fmt.Printf("job %q crashed: %v\n", name, r)
		}
	}()
	job()
	fmt.Printf("job %q done\n", name)
}

func main() {
	runJob("resize", func() {})
	runJob("parse", func() {
		var fields []string
		_ = strings.ToUpper(fields[2]) // a bug
	})
	runJob("email", func() {})
}
Output
job "resize" done
job "parse" crashed: runtime error: index out of range [2] with length 0
job "email" done

Recovering hides bugs if you're not careful: always log what happened. A panic in a goroutine can only be recovered by a deferred call in that same goroutine; otherwise it crashes the whole program (lesson 51).

When to panic yourself

Return errors for anything that can reasonably go wrong: bad input, missing files, network failures. Panic only for:

  • Programmer errors — a broken invariant that means the code itself is wrong, like an impossible switch case.
  • Setup that can't fail at run time, by convention in functions named Must…: regexp.MustCompile and template.Must panic on a bad pattern written into your source code, where an error return would just be noise.
GO
package main

import (
	"fmt"
	"strconv"
)

// MustAtoi is for values written into the program, never for user input.
func MustAtoi(s string) int {
	n, err := strconv.Atoi(s)
	if err != nil {
		panic(fmt.Sprintf("MustAtoi(%q): %v", s, err))
	}
	return n
}

var defaultLimit = MustAtoi("100")

func main() {
	fmt.Println(defaultLimit)
}
Output
100

Never let a panic cross a package's public API as a way of reporting ordinary failures: callers expect errors.

Errors versus panics

ErrorPanic
ForExpected failuresBugs, impossible states
Visible in the signatureYes, as a resultNo
Caller mustCheck itNothing — it propagates
Typical responseHandle, wrap, returnCrash and fix the code

Try it yourself

Run the program, then remove the deferred recover and see the difference.

Try it yourself · main.go
Edit the code, copy it into main.go in a new folder and run it with go run main.go (Go 1.27 or later). How to install Go
Output
processed a <nil>
processed b <nil>
 item 2: runtime error: index out of range [2] with length 2
Next: writing generic functions.