Part 32: Panic and Recover

Golang panic and recover Welcome to tutorial no. 32 in Golang tutorial series.

What is panic?

The idiomatic way to handle abnormal conditions in a program in Go is using errors. Errors are sufficient for most of the abnormal conditions arising in the program.

But there are some situations where the program cannot simply continue executing after an abnormal situation. In this case we use panic to terminate the program. When a function encounters a panic, it's execution is stopped, any deferred functions are executed and then the control returns to it's caller. This process continues until all the functions of the current goroutine have returned at which point the program prints the panic message, followed by the stack trace and then terminates. This concept will be more clear when we write a sample program.

It is possible to regain control of a panicking program using recover which we will discuss later in this tutorial.

panic and recover can be considered similar to try-catch-finally idiom in other languages except that it's rarely used and when used is more elegant and results in clean code.

When should panic be used?

One important factor is that you should avoid panic and recover and use errors where ever possible. Only in cases where the program just cannot continue execution should a panic and recover mechanism be used.

There are two valid use cases for panic.

  1. An unrecoverable error where the program cannot simply continue its execution.
    One example would be a web server which fails to bind to the required port. In this case it's reasonable to panic as there is nothing else to do if the port binding itself fails.

  2. A programmer error.
    Let's say we have a method which accepts a pointer as a parameter and someone calls this method using nil as argument. In this case we can panic as its a programmer error to call a method with nil argument which was expecting a valid pointer.

Panic example

The signature of the built in panic function is provided below,

func panic(interface{})  

The argument passed to panic will be printed when the program terminates. The use of this will be clear when we write a sample program. So let's do that right away.

We will start with a contrived example which shows how panic works.

package main

import (  
    "fmt"
)

func fullName(firstName *string, lastName *string) {  
    if firstName == nil {
        panic("runtime error: first name cannot be nil")
    }
    if lastName == nil {
        panic("runtime error: last name cannot be nil")
    }
    fmt.Printf("%s %s\n", *firstName, *lastName)
    fmt.Println("returned normally from fullName")
}

func main() {  
    firstName := "Elon"
    fullName(&firstName, nil)
    fmt.Println("returned normally from main")
}

Run in playground

The above is a simple program to print the full name of a person. The fullName function in line no. 7 prints the full name of a person. This function checks whether the firstName and lastName pointers are nil in line nos. 8 and 11 respectively. If it's nil the function calls panic with the corresponding error message. This error message will be printed when the program terminates.

Running this program will print the following output,

panic: runtime error: last name cannot be nil

goroutine 1 [running]:  
main.fullName(0x1040c128, 0x0)  
    /tmp/sandbox135038844/main.go:12 +0x120
main.main()  
    /tmp/sandbox135038844/main.go:20 +0x80

Let's analyze this output to understand how panic works and how the stack trace is printed when the program panics.

In line no. 19 we assign Elon to firstName. We call fullName function with lastName as nil in line no. 20. Hence the condition in line no. 11 will be satisfied and the program will panic. When a panic is encountered, the program execution terminates, prints the argument passed to panic followed by the stack trace. Hence the code in line nos. 14 and 15 will not be executed following the panic. This program first prints the message passed to the panic function,

panic: runtime error: last name cannot be empty  

and then prints the stack trace.

The program panicked in line no. 12 of fullName function and hence,

main.fullName(0x1040c128, 0x0)  
    /tmp/sandbox135038844/main.go:12 +0x120

will be printed first. Then the next item in the stack will be printed. In our case line no. 20 is the next item in the stack trace as the fullName call which caused the panic happened in this line and hence

main.main()  
    /tmp/sandbox135038844/main.go:20 +0x80

is printed next. Now we have reached the top level function which caused the panic and there are no more levels above, hence there is nothing more to print.



Defer while panicking

Let's recollect what a panic does. When a function encounters a panic, it's execution is stopped, any deferred functions are executed and then the control returns to it's caller. This process continues until all the functions of the current goroutine have returned at which point the program prints the panic message, followed by the stack trace and then terminates.

In the example above, we did not defer any function calls. If a deferred function call is present, it is executed and then the control returns to its caller.

Lets' modify the example above a little and use a defer statement.

package main

import (  
    "fmt"
)

func fullName(firstName *string, lastName *string) {  
    defer fmt.Println("deferred call in fullName")
    if firstName == nil {
        panic("runtime error: first name cannot be nil")
    }
    if lastName == nil {
        panic("runtime error: last name cannot be nil")
    }
    fmt.Printf("%s %s\n", *firstName, *lastName)
    fmt.Println("returned normally from fullName")
}

func main() {  
    defer fmt.Println("deferred call in main")
    firstName := "Elon"
    fullName(&firstName, nil)
    fmt.Println("returned normally from main")
}

Run in playground

The only changes made to the above program are the addition of the deferred function calls in line nos. 8 and 20.

This program prints,

deferred call in fullName  
deferred call in main  
panic: runtime error: last name cannot be nil

goroutine 1 [running]:  
main.fullName(0x1042bf90, 0x0)  
    /tmp/sandbox060731990/main.go:13 +0x280
main.main()  
    /tmp/sandbox060731990/main.go:22 +0xc0

When the program panics in line no. 13, any deferred function calls are first executed and then the control returns to the caller whose deferred calls are executed and so on until the top level caller is reached.

In our case defer statement in line no. 8 of fullName function is executed first. This prints

deferred call in fullName  

And then the control returns to the main function whose deferred calls are executed and hence this prints,

deferred call in main  

Now the control has reached the top level function and hence the program prints the panic message followed by the stack trace and then terminates.

Recover

recover is a builtin function which is used to regain control of a panicking goroutine.

The signature of recover function is provided below,

func recover() interface{}  

Recover is useful only when called inside deferred functions. Executing a call to recover inside a deferred function stops the panicking sequence by restoring normal execution and retrieves the error value passed to the call of panic. If recover is called outside the deferred function, it will not stop a panicking sequence.

Let's modify our program and use recover to restore normal execution after a panic.

package main

import (  
    "fmt"
)

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

func fullName(firstName *string, lastName *string) {  
    defer recoverName()
    if firstName == nil {
        panic("runtime error: first name cannot be nil")
    }
    if lastName == nil {
        panic("runtime error: last name cannot be nil")
    }
    fmt.Printf("%s %s\n", *firstName, *lastName)
    fmt.Println("returned normally from fullName")
}

func main() {  
    defer fmt.Println("deferred call in main")
    firstName := "Elon"
    fullName(&firstName, nil)
    fmt.Println("returned normally from main")
}

Run in playground

The recoverName() function in line no. 7 calls recover() which returns the value passed to the call of panic. Here we are just printing the value returned by recover in line no. 8. recoverName() is being deferred in line no. 14 inside the fullName function.

When fullName panics, the deferred function recoverName() will be called which uses recover() to stop the panicking sequence.

This program will print,

recovered from  runtime error: last name cannot be nil  
returned normally from main  
deferred call in main  

When the program panics in line no. 19, the deferred recoverName function will be called which in turn calls recover() to regain control of the panicking goroutine. The call to recover() in line no. 8 returns the argument from the panic and hence it prints,

recovered from  runtime error: last name cannot be nil  

After execution of recover(), the panicking stops and the control returns to the caller, in this case the main function and the program continues to execute normally from line 29 in main right after the panic. It prints returned normally from main followed by deferred call in main

Panic, Recover and Goroutines

Recover works only when it is called from the same goroutine. It's not possible to recover from a panic that has happened in a different goroutine. Let's understand this using an example.

package main

import (  
    "fmt"
    "time"
)

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

func a() {  
    defer recovery()
    fmt.Println("Inside A")
    go b()
    time.Sleep(1 * time.Second)
}

func b() {  
    fmt.Println("Inside B")
    panic("oh! B panicked")
}

func main() {  
    a()
    fmt.Println("normally returned from main")
}

Run in playground

In the program above, the function b() panics in line no. 23. The function a() calls a deferred function recovery() which is used to recover from panic. The function b() is called as a separate goroutine from line no. 17. and the Sleep in the next line is just to ensure that the program does not terminate before b() has finished running.

What do you think will be the output of the program. Will the panic be recovered? The answer is no. The panic will not be recovered. This is because the recovery function is present in a different gouroutine and the panic is happening in function b() in a different goroutine. Hence recovery is not possible.

Running this program will output,

Inside A  
Inside B  
panic: oh! B panicked

goroutine 5 [running]:  
main.b()  
    /tmp/sandbox388039916/main.go:23 +0x80
created by main.a  
    /tmp/sandbox388039916/main.go:17 +0xc0

You can see from the output that the recovery has not happened.

If the function b() was called in the same goroutine then the panic would have been recovered.

If the line no. 17 of the program is changed from

 go b()

to

b()  

the recovery will happen now since the panic is happening in the same goroutine. If the program is run with the above change it will output,

Inside A  
Inside B  
recovered: oh! B panicked  
normally returned from main  



Runtime panics

Panics can also be caused by runtime errors such as array out of bounds access. This is equivalent to a call of the built-in function panic with an argument defined by interface type runtime.Error. The definition of runtime.Error interface is provided below,

type Error interface {  
    error
    // RuntimeError is a no-op function but
    // serves to distinguish types that are run time
    // errors from ordinary errors: a type is a
    // run time error if it has a RuntimeError method.
    RuntimeError()
}

runtime.Error interface satisfies the built-in interface type error.

Lets write a contrived example which creates a runtime panic.

package main

import (  
    "fmt"
)

func a() {  
    n := []int{5, 7, 4}
    fmt.Println(n[3])
    fmt.Println("normally returned from a")
}
func main() {  
    a()
    fmt.Println("normally returned from main")
}

Run in playground

In the program above, in line no. 9 we are trying to access n[3] which is an invalid index in the slice. This program will panic with the following output,

panic: runtime error: index out of range

goroutine 1 [running]:  
main.a()  
    /tmp/sandbox780439659/main.go:9 +0x40
main.main()  
    /tmp/sandbox780439659/main.go:13 +0x20

You might be wondering whether it is possible to recover from a runtime panic. The answer is yes. Let's change the program above and recover from the panic.

package main

import (  
    "fmt"
)

func r() {  
    if r := recover(); r != nil {
        fmt.Println("Recovered", r)
    }
}

func a() {  
    defer r()
    n := []int{5, 7, 4}
    fmt.Println(n[3])
    fmt.Println("normally returned from a")
}

func main() {  
    a()
    fmt.Println("normally returned from main")
}

Run in playground

Running the above program will output,

Recovered runtime error: index out of range  
normally returned from main  

From the output you can understand that we have recovered from the panic.

Getting stack trace after recover

If we recover a panic, we loose the stack trace about the panic. Even in the program above after recovery, we lost the stack trace.

There is a way to print the stack trace using the PrintStack function of the Debug package

package main

import (  
    "fmt"
    "runtime/debug"
)

func r() {  
    if r := recover(); r != nil {
        fmt.Println("Recovered", r)
        debug.PrintStack()
    }
}

func a() {  
    defer r()
    n := []int{5, 7, 4}
    fmt.Println(n[3])
    fmt.Println("normally returned from a")
}

func main() {  
    a()
    fmt.Println("normally returned from main")
}

Run in playground

In the program above, we use debug.PrintStack() in line no.11 to print the stack trace.

This program will output,

Recovered runtime error: index out of range  
goroutine 1 [running]:  
runtime/debug.Stack(0x1042beb8, 0x2, 0x2, 0x1c)  
    /usr/local/go/src/runtime/debug/stack.go:24 +0xc0
runtime/debug.PrintStack()  
    /usr/local/go/src/runtime/debug/stack.go:16 +0x20
main.r()  
    /tmp/sandbox949178097/main.go:11 +0xe0
panic(0xf0a80, 0x17cd50)  
    /usr/local/go/src/runtime/panic.go:491 +0x2c0
main.a()  
    /tmp/sandbox949178097/main.go:18 +0x80
main.main()  
    /tmp/sandbox949178097/main.go:23 +0x20
normally returned from main  

From the output you can understand that first the panic is recovered and Recovered runtime error: index out of range is printed. Following that the stack trace is printed. Then normally returned from main is printed after the panic has recovered.

This brings us to an end of this tutorial.

Here is a quick recap of what we learned in this tutorial,

  • What is panic?
  • When should panic be used?
  • Panic example
  • Defer while panicking
  • Recover
  • Panic, Recover and Goroutines
  • Runtime panics
  • Getting stack trace after recover

Have a good day.