Skip to content
Theory · Functions as values · lesson 7 of 12

panic and recover

In the previous lesson defer was a to-do list by the door, read on the way out. Now picture a fire alarm. People leave the building floor by floor, but on each floor they go through the list by the door on their way out. Only someone whose list has the item "check and cancel the alarm" can cancel it.

Panic

In Go such an alarm is called a panic. Dividing an integer by zero while the program runs causes a panic. A panic interrupts the current function and every function that called it, but deferred calls still run on the way:

GO
package main

import "fmt"

func divide(a, b int) int {
    return a / b
}

func main() {
    defer fmt.Println("deferred call")
    fmt.Println(divide(6, 3)) // 2
    fmt.Println(divide(1, 0)) // ✗
}
TEXT
panic: runtime error: integer divide by zero

Let's go through what the program prints:

  1. divide(6, 3) returns 2, and the program prints 2.
  2. divide(1, 0) divides by zero, and a panic starts.
  3. The program prints deferred call.
  4. Only after that does the panic message appear.

The panic rises from divide into main, runs the deferred calls of main and ends the program. This climb up the chain of calls is called stack unwinding.

Where a panic comes from

A panic starts in two ways. The value it starts with can be anything.

A runtime error

Go itself starts a panic when an operation cannot be performed. Dividing an integer by zero is such a case. The value of this panic is a runtime error, and the message begins with runtime error:, as in the example above.

Calling panic

You can also start a panic yourself with the built-in panic function. The parentheses hold the panic value, such as a string. After panic("no config") the program stops with the message panic: no config.

The recover function

The built-in recover function stops a panic. It follows three rules:

  • it works only inside a deferred function;
  • during a panic it returns the value passed to panic, and the unwinding stops;
  • without a panic it returns nil.

A deferred function runs during the unwinding, so only there does recover catch the panic. This is the "cancel the alarm" item on the list by the door. The function whose deferred call stopped the panic finishes at once, and the code that called it carries on.

GO
func safeDivide(a, b int) (q int, ok bool) {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("recovered:", r)
        }
    }()
    return a / b, true
}

func main() {
    fmt.Println(safeDivide(6, 3)) // 2 true
    fmt.Println(safeDivide(1, 0)) // 0 false
}

Both results of a call can go straight into fmt.Println, with no variables in between: it prints them separated by a space. Let's go through both calls:

  1. safeDivide(6, 3) divides without a panic, recover returns nil, and 2 true is printed.
  2. safeDivide(1, 0) divides by zero, and the deferred function prints recovered: runtime error: integer divide by zero.
  3. Then 0 false is printed. The statement return a / b, true never completed, so the named results kept their zero values.

Here the fire-alarm picture ends. After a cancelled alarm, people go back to their floor and carry on working. But safeDivide does not return to the interrupted line: it finishes at once, and main gets its results as from an ordinary return and carries on from the next line.

An error instead of a panic

A deferred function can do more than print the panic: it can change the result. It sees the named results by name, and whatever it writes into them is what the function returns:

GO
func safeDivide(a, b int) (q int, err error) {
    defer func() {
        if r := recover(); r != nil {
            err = errors.New("division failed")
        }
    }()
    return a / b, nil
}

func main() {
    fmt.Println(safeDivide(6, 3)) // 2 <nil>
    fmt.Println(safeDivide(1, 0)) // 0 division failed
}

Let's go through the second call:

  1. Dividing by zero panics, and return a / b, nil never completes.
  2. The deferred function catches the panic and writes an error into err.
  3. safeDivide returns q and err as they are: 0 and division failed.

This way a panic inside the function reaches the calling code as an ordinary error.

Where recover works

Interviewers like to ask this. The answer has three parts:

  • recover works only in a deferred function;
  • it has to be called directly in that function's body;
  • anywhere else, recover returns nil and does not stop the panic.

Panic or error

Failures come in two kinds, and each has its own tool:

  • a panic—for errors that cannot happen in a correct program;
  • an error value—for an expected failure, such as bad input: the function returns it.

Common mistake

recover is called directly in the function body, not in a deferred function:

GO
func safeDivide(a, b int) int {
    recover() // ✗
    return a / b
}

The call safeDivide(1, 0) ends the program with the same integer divide by zero panic. Here is why:

  1. recover runs at the start of the body, when there is no panic yet, and returns nil.
  2. The panic on the next line is no longer stopped by anyone.

Call recover inside defer func() { … }(), as in safeDivide above.

Key points

  • A panic interrupts a function and rises up the chain of calls, running deferred calls on the way.
  • recover stops a panic only when it is called in a deferred function; without a panic it returns nil.
  • The function in which a panic was stopped finishes at once, and the code that called it carries on.
  • A deferred function can write an error into a named result, and the caller gets it instead of the panic.
  • Expected failures are returned as an error; a panic is for what cannot happen in a correct program.

Subscribe to take the course

The course is free. Sign in with Telegram and subscribe to the channel to open every lesson.

Sign in another way