Golang Panic and Recover
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
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 is 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.
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.A programmer error.
Let's say we have a method which accepts a pointer as a parameter and someone calls this method usingnil
as argument. In this case we can panic as it's a programmer error to call a method withnil
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")
}
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 is 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 nil
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, its execution is stopped, any deferred functions are executed and then the control returns to its 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.
Let's 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")
}
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")
}
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")
}
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.
Let's 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")
}
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")
}
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 lose 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")
}
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.
Please show your support by donating on Patreon. Your donations will help me create more awesome tutorials.