37. `panic` and `recover`

📖 Reading · 11 min
💡 Most code boxes below are live — edit one and hit Run. Boxes without a Run button are reference-only (they can't run in your browser).

Go does have a way to blow up the stack. It's called panic, it's reserved for situations that should never happen, and the most important thing to learn about it is when not to use it.

What a panic does

A panic stops the normal flow, runs every deferred call on the way out, and crashes the program with a stack trace.

package main

import "fmt"

func main() {
    defer fmt.Println("deferred calls still run")

    fmt.Println("before")
    panic("something went badly wrong")
}

Run it: you get before, then the deferred line, then the panic message. The deferred calls firing on the way out is what makes cleanup reliable even when things go wrong — and what makes recover possible.

The runtime panics on your behalf too: index out of range, nil map write, nil pointer dereference, integer divide by zero.

package main

import "fmt"

func main() {
    defer fmt.Println("cleanup ran")

    nums := []int{1, 2, 3}
    i := 10
    fmt.Println("about to index out of range...")
    fmt.Println(nums[i])
}

recover stops the unwinding

recover is only useful inside a deferred function. Called there during a panic, it returns the panic value and normal execution resumes in the caller:

package main

import "fmt"

func risky() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("recovered from:", r)
        }
    }()

    fmt.Println("starting risky work")
    panic("kaboom")
}

func main() {
    risky()
    fmt.Println("main is still running")
}

main continues. Note the shape — it's always this shape:

defer func() {
    if r := recover(); r != nil {
        // handle it
    }
}()

Called anywhere other than a deferred function, recover() returns nil and does nothing.

Turning a panic into an error

The one genuinely idiomatic use of recover: at a package boundary, convert a panic into a normal error so callers never see it.

package main

import (
    "fmt"
    "strings"
)

func parse(input string) (result int, err error) {
    defer func() {
        if r := recover(); r != nil {
            err = fmt.Errorf("parse %q: %v", input, r)
        }
    }()

    parts := strings.Split(input, "+")
    if len(parts) != 2 {
        panic("expected exactly one +")
    }

    digits := map[string]int{"one": 1, "two": 2, "three": 3}
    a, ok := digits[parts[0]]
    if !ok {
        panic("unknown word: " + parts[0])
    }
    b, ok := digits[parts[1]]
    if !ok {
        panic("unknown word: " + parts[1])
    }
    return a + b, nil
}

func main() {
    for _, in := range []string{"one+two", "one+seven", "nonsense"} {
        n, err := parse(in)
        if err != nil {
            fmt.Println("error:", err)
            continue
        }
        fmt.Printf("%s = %d\n", in, n)
    }
}

The named result err is what makes this work: the deferred closure runs after return has picked a value, and assigning to err changes what the caller receives. Same mechanism as deferred error wrapping in lesson 2.

This is how encoding/json and several parsers are built internally — panic through deep recursive code to avoid threading an error through every frame, then recover at the public function and hand back an error. Note that the panics stay inside the package. Users of parse see only errors.

The Must convention

The mirror image: a helper that panics on failure, used only where a failure means the program is broken and can't sensibly continue.

package main

import (
    "fmt"
    "regexp"
    "strconv"
)

func MustAtoi(s string) int {
    n, err := strconv.Atoi(s)
    if err != nil {
        panic(fmt.Sprintf("MustAtoi(%q): %v", s, err))
    }
    return n
}

var pattern = regexp.MustCompile(`^[a-z]+$`)

func main() {
    fmt.Println(MustAtoi("42") + 1)
    fmt.Println(pattern.MatchString("golang"), pattern.MatchString("Go1"))
}

regexp.MustCompile, template.Must and friends exist because a hard-coded regex that doesn't compile is a bug, discovered at startup, not a runtime condition to handle. That's the test for Must...: only for values known at compile time, in package-level variables or init.

When to panic, and when not to

Panic when the program cannot continue meaningfully:

  • an invariant your own code guarantees has been violated ("unreachable")
  • required configuration is missing at startup
  • a Must... helper with a hard-coded argument

Return an error for everything else — and "everything else" is nearly everything:

  • bad user input
  • a file that isn't there
  • a network call that failed
  • anything a caller could reasonably want to handle
package main

import (
    "errors"
    "fmt"
)

func divideBad(a, b int) int {
    if b == 0 {
        panic("division by zero")
    }
    return a / b
}

func divideGood(a, b int) (int, error) {
    if b == 0 {
        return 0, errors.New("division by zero")
    }
    return a / b, nil
}

func main() {
    if q, err := divideGood(10, 0); err != nil {
        fmt.Println("handled cleanly:", err)
    } else {
        fmt.Println(q)
    }

    fmt.Println(divideGood(10, 2))
    fmt.Println(divideBad(10, 2))
}

A library that panics on bad input takes the choice away from its caller. That's why the standard library almost never does it.

Two more rules worth knowing:

  • Don't recover just to keep going. Recovering and continuing as if nothing happened leaves your program in an unknown state. Recover to convert, log, and return — or don't recover at all.
  • A panic in a goroutine kills the whole program, even if the goroutine that started it has a recover. Each goroutine needs its own deferred recover if it might panic. That comes up again in the next module.

Your turn

Complete safeDivide so a division-by-zero panic is recovered and returned as an error:

5 <nil>
0 recovered: runtime error: integer divide by zero
package main

import "fmt"

func safeDivide(a, b int) (result int, err error) {
    // defer a closure that recovers and sets err to
    // fmt.Errorf("recovered: %v", r)
    return a / b, nil
}

func main() {
    q, err := safeDivide(10, 2)
    fmt.Println(q, err)

    q, err = safeDivide(10, 0)
    fmt.Println(q, err)
}
package main

import "fmt"

func safeDivide(a, b int) (result int, err error) {
    defer func() {
        if r := recover(); r != nil {
            result = 0
            err = fmt.Errorf("recovered: %v", r)
        }
    }()
    return a / b, nil
}

func main() {
    q, err := safeDivide(10, 2)
    fmt.Println(q, err)

    q, err = safeDivide(10, 0)
    fmt.Println(q, err)
}

You can now fail well. Next module: *, &, and what actually lives where — pointers and memory.